sendou.ink/app/features/notifications/NotificationRepository.server.ts
Kalle 881c53d961 Enforce homogeneous multi-row inserts at the type level
Kysely fills keys missing from some rows of a multi-row .values() with
literal null on SQLite (it cannot emit the DEFAULT keyword), which was
the cause of the organizer registration crash. The kysely patch now
rejects row arrays whose element type has optional or heterogeneous
keys, with an error naming the offending column.
2026-08-03 07:28:13 +03:00

119 lines
3.0 KiB
TypeScript

import { sub } from "date-fns";
import { db } from "~/db/sql";
import type { TablesInsertable } from "~/db/tables";
import type { NotificationSubscription } from "~/db/tables-json";
import { actorId } from "~/features/auth/core/user.server";
import { dateToDatabaseTimestamp } from "../../utils/dates";
import { NOTIFICATIONS } from "./notifications-contants";
import type { Notification } from "./notifications-types";
export function insert(
notification: Notification,
users: Array<Omit<TablesInsertable["NotificationUser"], "notificationId">>,
) {
return db.transaction().execute(async (trx) => {
const inserted = await trx
.insertInto("Notification")
.values({
type: notification.type,
pictureUrl: notification.pictureUrl,
meta: notification.meta ? JSON.stringify(notification.meta) : null,
})
.returning("id")
.executeTakeFirstOrThrow();
await trx
.insertInto("NotificationUser")
.values(
users.map(({ userId, seen }) => ({
userId,
notificationId: inserted.id,
seen: seen ?? 0,
})),
)
.execute();
return inserted;
});
}
export function findByUserId(
userId: number,
{ limit }: { limit?: number } = {},
) {
return db
.selectFrom("NotificationUser")
.innerJoin(
"Notification",
"Notification.id",
"NotificationUser.notificationId",
)
.select([
"Notification.id",
"Notification.createdAt",
"NotificationUser.seen",
"Notification.type",
"Notification.meta",
"Notification.pictureUrl",
])
.where("NotificationUser.userId", "=", userId)
.limit(limit ?? NOTIFICATIONS.MAX_SHOWN)
.orderBy("Notification.id", "desc")
.execute() as Promise<
Array<Notification & { id: number; createdAt: number; seen: number }>
>;
}
export function findAllByType<T extends Notification["type"]>(type: T) {
return db
.selectFrom("Notification")
.select(["type", "meta", "pictureUrl"])
.where("type", "=", type)
.execute() as Promise<Array<Extract<Notification, { type: T }>>>;
}
export function markOwnAsSeen(notificationIds: number[]) {
return db
.updateTable("NotificationUser")
.set("seen", 1)
.where("NotificationUser.notificationId", "in", notificationIds)
.where("NotificationUser.userId", "=", actorId())
.execute();
}
export function deleteOld() {
return db
.deleteFrom("Notification")
.where(
"createdAt",
"<",
dateToDatabaseTimestamp(sub(new Date(), { days: 14 })),
)
.executeTakeFirst();
}
export function insertOwnSubscription(subscription: NotificationSubscription) {
return db
.insertInto("NotificationUserSubscription")
.values({
userId: actorId(),
subscription: JSON.stringify(subscription),
})
.execute();
}
export function findAllSubscriptionsByUserIds(userIds: number[]) {
return db
.selectFrom("NotificationUserSubscription")
.select(["id", "subscription"])
.where("userId", "in", userIds)
.execute();
}
export function deleteSubscriptionById(id: number) {
return db
.deleteFrom("NotificationUserSubscription")
.where("id", "=", id)
.execute();
}