-
Notifications
You must be signed in to change notification settings - Fork 4
update dependencies and add notification events setting #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| CLOUDFLARE_ACCOUNT_ID= | ||
| CLOUDFLARE_DATABASE_ID= | ||
| CLOUDFLARE_TOKEN= | ||
| CLOUDFLARE_TOKEN= | ||
| # 本地开发时取消下面的注释 | ||
| # CLOUDFLARE_ENV=development | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||
| import { and, desc, eq, inArray, lt } from "drizzle-orm"; | ||||||||||||||||||||||||||||||||||||
| import { getDb } from "@/db"; | ||||||||||||||||||||||||||||||||||||
| import { notificationChannels, notificationEvents, subscriptions } from "@/db/schema"; | ||||||||||||||||||||||||||||||||||||
| import { requireApiAuth } from "@/lib/session"; | ||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const cleanupSchema = z.object({ | ||||||||||||||||||||||||||||||||||||
| months: z.number().int().min(1), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export async function GET() { | ||||||||||||||||||||||||||||||||||||
| const session = await requireApiAuth(); | ||||||||||||||||||||||||||||||||||||
| const db = await getDb(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const items = await db | ||||||||||||||||||||||||||||||||||||
| .select({ | ||||||||||||||||||||||||||||||||||||
| id: notificationEvents.id, | ||||||||||||||||||||||||||||||||||||
| createdAt: notificationEvents.createdAt, | ||||||||||||||||||||||||||||||||||||
| sentAt: notificationEvents.sentAt, | ||||||||||||||||||||||||||||||||||||
| subscriptionName: subscriptions.name, | ||||||||||||||||||||||||||||||||||||
| channelName: notificationChannels.name, | ||||||||||||||||||||||||||||||||||||
| status: notificationEvents.status, | ||||||||||||||||||||||||||||||||||||
| offsetDays: notificationEvents.offsetDays, | ||||||||||||||||||||||||||||||||||||
| error: notificationEvents.error, | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| .from(notificationEvents) | ||||||||||||||||||||||||||||||||||||
| .innerJoin(subscriptions, eq(notificationEvents.subscriptionId, subscriptions.id)) | ||||||||||||||||||||||||||||||||||||
| .innerJoin(notificationChannels, eq(notificationEvents.channelId, notificationChannels.id)) | ||||||||||||||||||||||||||||||||||||
| .where(eq(subscriptions.userId, session.userId)) | ||||||||||||||||||||||||||||||||||||
| .orderBy(desc(notificationEvents.createdAt)) | ||||||||||||||||||||||||||||||||||||
| .limit(50); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ items }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export async function DELETE(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||
| const session = await requireApiAuth(); | ||||||||||||||||||||||||||||||||||||
| const db = await getDb(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const body = await request.json() as unknown; | ||||||||||||||||||||||||||||||||||||
| const parsed = cleanupSchema.safeParse(body); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!parsed.success) { | ||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against malformed JSON bodies.
🛡️ Proposed fix- const body = await request.json() as unknown;
- const parsed = cleanupSchema.safeParse(body);
+ let body: unknown;
+ try {
+ body = await request.json();
+ } catch {
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+ const parsed = cleanupSchema.safeParse(body);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const cutoff = new Date(); | ||||||||||||||||||||||||||||||||||||
| cutoff.setMonth(cutoff.getMonth() - parsed.data.months); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Subtracting months from a date like Mar 31 yields Mar 3 (since Feb has no 31st), so the cutoff can end up slightly inside the current month. Typically harmless for a retention/cleanup window, but worth being aware of; consider normalizing to day 1 or using a day-based offset if exact semantics matter. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const ids = await db | ||||||||||||||||||||||||||||||||||||
| .select({ id: notificationEvents.id }) | ||||||||||||||||||||||||||||||||||||
| .from(notificationEvents) | ||||||||||||||||||||||||||||||||||||
| .innerJoin(subscriptions, eq(notificationEvents.subscriptionId, subscriptions.id)) | ||||||||||||||||||||||||||||||||||||
| .where(eq(subscriptions.userId, session.userId)); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const deletableIds = ids.map((item) => item.id); | ||||||||||||||||||||||||||||||||||||
| if (deletableIds.length === 0) { | ||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ success: true, deletedCount: 0 }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const deleted = await db | ||||||||||||||||||||||||||||||||||||
| .delete(notificationEvents) | ||||||||||||||||||||||||||||||||||||
| .where( | ||||||||||||||||||||||||||||||||||||
| and( | ||||||||||||||||||||||||||||||||||||
| inArray(notificationEvents.id, deletableIds), | ||||||||||||||||||||||||||||||||||||
| lt(notificationEvents.createdAt, cutoff) | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| .returning({ id: notificationEvents.id }); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current cleanup logic is inefficient and does not scale well. It fetches all notification event IDs for the user into application memory (without even filtering by the cutoff date) and then performs a deletion using an It is recommended to perform the deletion in a single query using a subquery. This avoids fetching unnecessary data and reduces database round-trips. const deleted = await db
.delete(notificationEvents)
.where(
and(
lt(notificationEvents.createdAt, cutoff),
inArray(
notificationEvents.subscriptionId,
db.select({ id: subscriptions.id })
.from(subscriptions)
.where(eq(subscriptions.userId, session.userId))
)
)
)
.returning({ id: notificationEvents.id });
Comment on lines
+51
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Drizzle ORM's inArray does not directly support subqueries as the second argument. Users have reported issues attempting this, with no TypeScript errors but runtime failures. Instead, Drizzle supports subqueries in WHERE clauses via exists, notExists, or raw sqlcolumn IN (subquery) templates. For example: ts const userIds = db.select({ id: posts.userId }).from(posts).as('user_ids'); await db.select.from(users).where(inArray(users.id, sql(SELECT id FROM user_ids))); // or use exists However, the standard workaround for large arrays in inArray is manual chunking due to Cloudflare D1's limit. Cloudflare D1 enforces a maximum of 100 bound parameters per query (SQLITE_MAX_VARIABLE_NUMBER equivalent), as stated in official docs. This affects Drizzle's inArray, where each array element becomes a bound parameter (e.g., WHERE id IN (?, ?, ...) with N parameters for N IDs). For inArray with >100 IDs on D1, queries fail with "too many SQL variables" errors. Drizzle does not auto-chunk; users must manually chunk arrays and execute multiple queries or use db.batch. Examples from issues: ts const BATCH_SIZE = 90; // Conservative, accounting for other params const chunks = chunk(ids, BATCH_SIZE); for (const chunk of chunks) { await db.select.from(table).where(inArray(table.id, chunk)); } Subqueries avoid parameter limits entirely since they execute separately, making them ideal for large/dynamic sets on D1. Use subqueries or chunking to work around the 100-parameter limit. Citations:
🏁 Script executed: head -50 app/api/settings/notification-events/route.tsRepository: Merack/subflare-vinext Length of output: 1727 🏁 Script executed: rg "exists|inArray|sql\(" app/api/settings/notification-events/route.ts -A 2Repository: Merack/subflare-vinext Length of output: 354 🏁 Script executed: rg "from drizzle|import.*drizzle" app/api/settings/notification-events/route.tsRepository: Merack/subflare-vinext Length of output: 123 Use a subquery with The current approach loads every notification event ID belonging to the user into a JavaScript array and passes it to Instead, use a subquery with ♻️ Proposed refactor+import { exists } from "drizzle-orm";
import { and, desc, eq, inArray, lt } from "drizzle-orm";
- const ids = await db
- .select({ id: notificationEvents.id })
- .from(notificationEvents)
- .innerJoin(subscriptions, eq(notificationEvents.subscriptionId, subscriptions.id))
- .where(eq(subscriptions.userId, session.userId));
-
- const deletableIds = ids.map((item) => item.id);
- if (deletableIds.length === 0) {
- return NextResponse.json({ success: true, deletedCount: 0 });
- }
-
const deleted = await db
.delete(notificationEvents)
.where(
and(
- inArray(notificationEvents.id, deletableIds),
+ exists(
+ db.select({ id: subscriptions.id })
+ .from(subscriptions)
+ .where(
+ and(
+ eq(subscriptions.userId, session.userId),
+ eq(subscriptions.id, notificationEvents.subscriptionId)
+ )
+ )
+ ),
lt(notificationEvents.createdAt, cutoff)
)
)
.returning({ id: notificationEvents.id });🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ success: true, deletedCount: deleted.length }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a trailing newline.
Per the dotenv-linter hint, the file is missing an ending blank line.
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 5-5: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🤖 Prompt for AI Agents