initttt
This commit is contained in:
96
packages/logic/domains/notifications/controller.ts
Normal file
96
packages/logic/domains/notifications/controller.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { FlowExecCtx } from "@/core/flow.execution.context";
|
||||
import { okAsync } from "neverthrow";
|
||||
import {
|
||||
NotificationFilters,
|
||||
PaginationOptions,
|
||||
} from "./data";
|
||||
import { NotificationRepository } from "./repository";
|
||||
import { db } from "@pkg/db";
|
||||
|
||||
export class NotificationController {
|
||||
constructor(private notifsRepo: NotificationRepository) {}
|
||||
|
||||
getNotifications(
|
||||
fctx: FlowExecCtx,
|
||||
filters: NotificationFilters,
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
return this.notifsRepo.getNotifications(fctx, filters, pagination);
|
||||
}
|
||||
|
||||
markAsRead(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.markAsRead(fctx, notificationIds, userId);
|
||||
}
|
||||
|
||||
markAsUnread(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.markAsUnread(fctx, notificationIds, userId);
|
||||
}
|
||||
|
||||
archive(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.archive(fctx, notificationIds, userId);
|
||||
}
|
||||
|
||||
unarchive(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.unarchive(fctx, notificationIds, userId);
|
||||
}
|
||||
|
||||
deleteNotifications(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.deleteNotifications(fctx, notificationIds, userId);
|
||||
}
|
||||
|
||||
getUnreadCount(
|
||||
fctx: FlowExecCtx,
|
||||
userId: string,
|
||||
) {
|
||||
return this.notifsRepo.getUnreadCount(fctx, userId);
|
||||
}
|
||||
|
||||
markAllAsRead(
|
||||
fctx: FlowExecCtx,
|
||||
userId: string,
|
||||
) {
|
||||
// Get all unread notification IDs for this user
|
||||
const filters: NotificationFilters = {
|
||||
userId,
|
||||
isRead: false,
|
||||
isArchived: false,
|
||||
};
|
||||
|
||||
// Get a large number to handle bulk operations
|
||||
const pagination: PaginationOptions = { page: 1, pageSize: 1000 };
|
||||
|
||||
return this.notifsRepo
|
||||
.getNotifications(fctx, filters, pagination)
|
||||
.map((paginated) => paginated.data.map((n) => n.id))
|
||||
.andThen((notificationIds) => {
|
||||
if (notificationIds.length === 0) {
|
||||
return okAsync(true);
|
||||
}
|
||||
return this.notifsRepo.markAsRead(fctx, notificationIds, userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getNotificationController(): NotificationController {
|
||||
return new NotificationController(new NotificationRepository(db));
|
||||
}
|
||||
115
packages/logic/domains/notifications/data.ts
Normal file
115
packages/logic/domains/notifications/data.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as v from "valibot";
|
||||
|
||||
// Notification schema
|
||||
export const notificationSchema = v.object({
|
||||
id: v.pipe(v.number(), v.integer()),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
priority: v.string(),
|
||||
type: v.string(),
|
||||
category: v.string(),
|
||||
isRead: v.boolean(),
|
||||
isArchived: v.boolean(),
|
||||
actionUrl: v.string(),
|
||||
actionType: v.string(),
|
||||
actionData: v.string(),
|
||||
icon: v.string(),
|
||||
userId: v.string(),
|
||||
sentAt: v.date(),
|
||||
readAt: v.nullable(v.date()),
|
||||
expiresAt: v.nullable(v.date()),
|
||||
createdAt: v.date(),
|
||||
updatedAt: v.date(),
|
||||
});
|
||||
|
||||
export type Notification = v.InferOutput<typeof notificationSchema>;
|
||||
export type Notifications = Notification[];
|
||||
|
||||
// Notification filters schema
|
||||
export const notificationFiltersSchema = v.object({
|
||||
userId: v.string(),
|
||||
isRead: v.optional(v.boolean()),
|
||||
isArchived: v.optional(v.boolean()),
|
||||
type: v.optional(v.string()),
|
||||
category: v.optional(v.string()),
|
||||
priority: v.optional(v.string()),
|
||||
search: v.optional(v.string()),
|
||||
});
|
||||
export type NotificationFilters = v.InferOutput<
|
||||
typeof notificationFiltersSchema
|
||||
>;
|
||||
|
||||
export type NotificationsQueryInput = {
|
||||
isRead?: boolean;
|
||||
isArchived?: boolean;
|
||||
type?: string;
|
||||
category?: string;
|
||||
priority?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
};
|
||||
|
||||
// Pagination options schema
|
||||
export const paginationOptionsSchema = v.object({
|
||||
page: v.pipe(v.number(), v.integer()),
|
||||
pageSize: v.pipe(v.number(), v.integer()),
|
||||
sortBy: v.optional(v.string()),
|
||||
sortOrder: v.optional(v.string()),
|
||||
});
|
||||
export type PaginationOptions = v.InferOutput<typeof paginationOptionsSchema>;
|
||||
|
||||
// Paginated notifications schema
|
||||
export const paginatedNotificationsSchema = v.object({
|
||||
data: v.array(notificationSchema),
|
||||
total: v.pipe(v.number(), v.integer()),
|
||||
page: v.pipe(v.number(), v.integer()),
|
||||
pageSize: v.pipe(v.number(), v.integer()),
|
||||
totalPages: v.pipe(v.number(), v.integer()),
|
||||
});
|
||||
export type PaginatedNotifications = v.InferOutput<
|
||||
typeof paginatedNotificationsSchema
|
||||
>;
|
||||
|
||||
// Get notifications schema
|
||||
export const getNotificationsSchema = v.object({
|
||||
filters: notificationFiltersSchema,
|
||||
pagination: paginationOptionsSchema,
|
||||
});
|
||||
export type GetNotifications = v.InferOutput<typeof getNotificationsSchema>;
|
||||
|
||||
// Bulk notification IDs schema
|
||||
export const bulkNotificationIdsSchema = v.object({
|
||||
notificationIds: v.array(v.pipe(v.number(), v.integer())),
|
||||
});
|
||||
export type BulkNotificationIds = v.InferOutput<
|
||||
typeof bulkNotificationIdsSchema
|
||||
>;
|
||||
|
||||
// View Model specific types
|
||||
export const clientNotificationFiltersSchema = v.object({
|
||||
userId: v.string(),
|
||||
isRead: v.optional(v.boolean()),
|
||||
isArchived: v.optional(v.boolean()),
|
||||
type: v.optional(v.string()),
|
||||
category: v.optional(v.string()),
|
||||
priority: v.optional(v.string()),
|
||||
search: v.optional(v.string()),
|
||||
});
|
||||
export type ClientNotificationFilters = v.InferOutput<
|
||||
typeof clientNotificationFiltersSchema
|
||||
>;
|
||||
|
||||
export const clientPaginationStateSchema = v.object({
|
||||
page: v.pipe(v.number(), v.integer()),
|
||||
pageSize: v.pipe(v.number(), v.integer()),
|
||||
total: v.pipe(v.number(), v.integer()),
|
||||
totalPages: v.pipe(v.number(), v.integer()),
|
||||
sortBy: v.picklist(["createdAt", "sentAt", "readAt", "priority"]),
|
||||
sortOrder: v.picklist(["asc", "desc"]),
|
||||
});
|
||||
export type ClientPaginationState = v.InferOutput<
|
||||
typeof clientPaginationStateSchema
|
||||
>;
|
||||
78
packages/logic/domains/notifications/errors.ts
Normal file
78
packages/logic/domains/notifications/errors.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { FlowExecCtx } from "@/core/flow.execution.context";
|
||||
import { ERROR_CODES, type Err } from "@pkg/result";
|
||||
import { getError } from "@pkg/logger";
|
||||
|
||||
export const notificationErrors = {
|
||||
dbError: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Database operation failed",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
getNotificationsFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch notifications",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
markAsReadFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to mark notifications as read",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
markAsUnreadFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to mark notifications as unread",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
archiveFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to archive notifications",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
unarchiveFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to unarchive notifications",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
deleteNotificationsFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to delete notifications",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
|
||||
getUnreadCountFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||
getError({
|
||||
flowId: fctx.flowId,
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to get unread count",
|
||||
description: "Please try again later",
|
||||
detail,
|
||||
}),
|
||||
};
|
||||
|
||||
453
packages/logic/domains/notifications/repository.ts
Normal file
453
packages/logic/domains/notifications/repository.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
import { and, asc, count, Database, desc, eq, like, or, sql } from "@pkg/db";
|
||||
import { notifications } from "@pkg/db/schema";
|
||||
import { ResultAsync } from "neverthrow";
|
||||
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||
import type {
|
||||
Notification,
|
||||
NotificationFilters,
|
||||
PaginatedNotifications,
|
||||
PaginationOptions,
|
||||
} from "./data";
|
||||
import { type Err } from "@pkg/result";
|
||||
import { notificationErrors } from "./errors";
|
||||
import { logDomainEvent } from "@pkg/logger";
|
||||
|
||||
export class NotificationRepository {
|
||||
constructor(private db: Database) {}
|
||||
|
||||
getNotifications(
|
||||
fctx: FlowExecCtx,
|
||||
filters: NotificationFilters,
|
||||
pagination: PaginationOptions,
|
||||
): ResultAsync<PaginatedNotifications, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.list.started",
|
||||
fctx,
|
||||
meta: {
|
||||
hasSearch: Boolean(filters.search),
|
||||
isRead: filters.isRead,
|
||||
isArchived: filters.isArchived,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
sortBy: pagination.sortBy,
|
||||
sortOrder: pagination.sortOrder,
|
||||
},
|
||||
});
|
||||
|
||||
const { userId, isRead, isArchived, type, category, priority, search } =
|
||||
filters;
|
||||
const {
|
||||
page,
|
||||
pageSize,
|
||||
sortBy = "createdAt",
|
||||
sortOrder = "desc",
|
||||
} = pagination;
|
||||
|
||||
// Build WHERE conditions
|
||||
const conditions = [eq(notifications.userId, userId)];
|
||||
|
||||
if (isRead !== undefined) {
|
||||
conditions.push(eq(notifications.isRead, isRead));
|
||||
}
|
||||
|
||||
if (isArchived !== undefined) {
|
||||
conditions.push(eq(notifications.isArchived, isArchived));
|
||||
}
|
||||
|
||||
if (type) {
|
||||
conditions.push(eq(notifications.type, type));
|
||||
}
|
||||
|
||||
if (category) {
|
||||
conditions.push(eq(notifications.category, category));
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
conditions.push(eq(notifications.priority, priority));
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push(
|
||||
or(
|
||||
like(notifications.title, `%${search}%`),
|
||||
like(notifications.body, `%${search}%`),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause = and(...conditions);
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db.select({ count: count() }).from(notifications).where(whereClause),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.list.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.getNotificationsFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).andThen((totalResult) => {
|
||||
const total = totalResult[0]?.count || 0;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// Map sortBy to proper column
|
||||
const getOrderColumn = (sortBy: string) => {
|
||||
switch (sortBy) {
|
||||
case "createdAt":
|
||||
return notifications.createdAt;
|
||||
case "sentAt":
|
||||
return notifications.sentAt;
|
||||
case "readAt":
|
||||
return notifications.readAt;
|
||||
case "priority":
|
||||
return notifications.priority;
|
||||
default:
|
||||
return notifications.createdAt;
|
||||
}
|
||||
};
|
||||
|
||||
const orderColumn = getOrderColumn(sortBy);
|
||||
const orderFunc = sortOrder === "asc" ? asc : desc;
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.select()
|
||||
.from(notifications)
|
||||
.where(whereClause)
|
||||
.orderBy(orderFunc(orderColumn))
|
||||
.limit(pageSize)
|
||||
.offset(offset),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.list.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.getNotificationsFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map((data) => {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
logDomainEvent({
|
||||
event: "notifications.list.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: {
|
||||
count: data.length,
|
||||
page,
|
||||
totalPages,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: data as Notification[],
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
markAsRead(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
): ResultAsync<boolean, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.mark_read.started",
|
||||
fctx,
|
||||
meta: { userId, notificationCount: notificationIds.length },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.update(notifications)
|
||||
.set({
|
||||
isRead: true,
|
||||
readAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
sql`${notifications.id} = ANY(${notificationIds})`,
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.mark_read.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.markAsReadFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map(() => {
|
||||
logDomainEvent({
|
||||
event: "notifications.mark_read.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { notificationCount: notificationIds.length },
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
markAsUnread(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
): ResultAsync<boolean, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.mark_unread.started",
|
||||
fctx,
|
||||
meta: { userId, notificationCount: notificationIds.length },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.update(notifications)
|
||||
.set({
|
||||
isRead: false,
|
||||
readAt: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
sql`${notifications.id} = ANY(${notificationIds})`,
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.mark_unread.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.markAsUnreadFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map(() => {
|
||||
logDomainEvent({
|
||||
event: "notifications.mark_unread.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { notificationCount: notificationIds.length },
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
archive(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
): ResultAsync<boolean, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.archive.started",
|
||||
fctx,
|
||||
meta: { userId, notificationCount: notificationIds.length },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.update(notifications)
|
||||
.set({
|
||||
isArchived: true,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
sql`${notifications.id} = ANY(${notificationIds})`,
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.archive.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.archiveFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map(() => {
|
||||
logDomainEvent({
|
||||
event: "notifications.archive.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { notificationCount: notificationIds.length },
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
unarchive(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
): ResultAsync<boolean, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.unarchive.started",
|
||||
fctx,
|
||||
meta: { userId, notificationCount: notificationIds.length },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.update(notifications)
|
||||
.set({
|
||||
isArchived: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
sql`${notifications.id} = ANY(${notificationIds})`,
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.unarchive.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.unarchiveFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map(() => {
|
||||
logDomainEvent({
|
||||
event: "notifications.unarchive.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { notificationCount: notificationIds.length },
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
deleteNotifications(
|
||||
fctx: FlowExecCtx,
|
||||
notificationIds: number[],
|
||||
userId: string,
|
||||
): ResultAsync<boolean, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.delete.started",
|
||||
fctx,
|
||||
meta: { userId, notificationCount: notificationIds.length },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.delete(notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
sql`${notifications.id} = ANY(${notificationIds})`,
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.delete.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.deleteNotificationsFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map(() => {
|
||||
logDomainEvent({
|
||||
event: "notifications.delete.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { notificationCount: notificationIds.length },
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
getUnreadCount(
|
||||
fctx: FlowExecCtx,
|
||||
userId: string,
|
||||
): ResultAsync<number, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "notifications.unread_count.started",
|
||||
fctx,
|
||||
meta: { userId },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.select({ count: count() })
|
||||
.from(notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.userId, userId),
|
||||
eq(notifications.isRead, false),
|
||||
eq(notifications.isArchived, false),
|
||||
),
|
||||
),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "notifications.unread_count.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
});
|
||||
return notificationErrors.getUnreadCountFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).map((result) => {
|
||||
const count = result[0]?.count || 0;
|
||||
logDomainEvent({
|
||||
event: "notifications.unread_count.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { count },
|
||||
});
|
||||
return count;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user