Files
iistwin/server/storage/social.storage.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

385 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { users, userNotifications, bookmarkFolders, bookmarks, type User, type UserNotification, type BookmarkFolder, type Bookmark, type InsertUserNotification, type InsertBookmarkFolder, type InsertBookmark } from "@shared/schema";
import { db } from "../db";
import { eq, and, desc, isNull, sql } from "drizzle-orm";
import crypto from "crypto";
import { TasksStorage } from "./tasks.storage";
export class SocialStorage extends TasksStorage {
// Bookmark Folders
async getBookmarkFolder(id: number, organizationId: number, userId: number): Promise<BookmarkFolder | undefined> {
const [folder] = await db
.select()
.from(bookmarkFolders)
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
return folder || undefined;
}
async getBookmarkFoldersByOrganization(organizationId: number, userId: number): Promise<BookmarkFolder[]> {
return await db
.select()
.from(bookmarkFolders)
.where(and(eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
.orderBy(bookmarkFolders.position);
}
async getBookmarkFoldersByParent(parentId: number | null, organizationId: number, userId: number): Promise<BookmarkFolder[]> {
const whereCondition = parentId
? and(eq(bookmarkFolders.parentId, parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId))
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId));
return await db
.select()
.from(bookmarkFolders)
.where(whereCondition)
.orderBy(bookmarkFolders.position);
}
async createBookmarkFolder(insertFolder: InsertBookmarkFolder): Promise<BookmarkFolder> {
const whereCondition = insertFolder.parentId != null
? and(eq(bookmarkFolders.parentId, insertFolder.parentId), eq(bookmarkFolders.organizationId, insertFolder.organizationId), eq(bookmarkFolders.userId, insertFolder.userId))
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, insertFolder.organizationId), eq(bookmarkFolders.userId, insertFolder.userId));
const [maxRow] = await db
.select({ maxPos: sql<number>`coalesce(max(${bookmarkFolders.position}), -1)` })
.from(bookmarkFolders)
.where(whereCondition);
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
const [folder] = await db
.insert(bookmarkFolders)
.values({ ...insertFolder, position: nextPosition })
.returning();
return folder;
}
async updateBookmarkFolder(id: number, organizationId: number, updates: Partial<BookmarkFolder>, userId: number): Promise<BookmarkFolder> {
const [folder] = await db
.update(bookmarkFolders)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
.returning();
return folder;
}
async deleteBookmarkFolder(id: number, organizationId: number, userId: number): Promise<void> {
await db.transaction(async (tx) => {
const deleted = await tx
.delete(bookmarkFolders)
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
.returning({ parentId: bookmarkFolders.parentId });
if (deleted.length === 0) return;
const parentId = deleted[0].parentId;
const whereCondition = parentId != null
? and(eq(bookmarkFolders.parentId, parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId))
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId));
const remaining = await tx
.select({ id: bookmarkFolders.id, position: bookmarkFolders.position })
.from(bookmarkFolders)
.where(whereCondition)
.orderBy(bookmarkFolders.position, bookmarkFolders.id);
for (let idx = 0; idx < remaining.length; idx++) {
if (remaining[idx].position !== idx) {
await tx
.update(bookmarkFolders)
.set({ position: idx, updatedAt: new Date() })
.where(and(eq(bookmarkFolders.id, remaining[idx].id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
}
}
});
}
// Bookmarks
async getBookmark(id: number, organizationId: number, userId: number): Promise<Bookmark | undefined> {
const [bookmark] = await db
.select()
.from(bookmarks)
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
return bookmark || undefined;
}
async getBookmarksByFolder(folderId: number | null, organizationId: number, userId: number): Promise<Bookmark[]> {
const whereCondition = folderId
? and(eq(bookmarks.folderId, folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId))
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId));
return await db
.select()
.from(bookmarks)
.where(whereCondition)
.orderBy(bookmarks.position, bookmarks.id);
}
async getBookmarksByOrganization(organizationId: number, userId: number): Promise<Bookmark[]> {
return await db
.select()
.from(bookmarks)
.where(and(eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
.orderBy(bookmarks.position, bookmarks.id);
}
async createBookmark(insertBookmark: InsertBookmark): Promise<Bookmark> {
const whereCondition = insertBookmark.folderId != null
? and(eq(bookmarks.folderId, insertBookmark.folderId), eq(bookmarks.organizationId, insertBookmark.organizationId), eq(bookmarks.userId, insertBookmark.userId))
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, insertBookmark.organizationId), eq(bookmarks.userId, insertBookmark.userId));
const [maxRow] = await db
.select({ maxPos: sql<number>`coalesce(max(${bookmarks.position}), -1)` })
.from(bookmarks)
.where(whereCondition);
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
const [bookmark] = await db
.insert(bookmarks)
.values({ ...insertBookmark, position: nextPosition })
.returning();
return bookmark;
}
async updateBookmark(id: number, organizationId: number, updates: Partial<Bookmark>, userId: number): Promise<Bookmark> {
const [bookmark] = await db
.update(bookmarks)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
.returning();
return bookmark;
}
async deleteBookmark(id: number, organizationId: number, userId: number): Promise<void> {
await db.transaction(async (tx) => {
const deleted = await tx
.delete(bookmarks)
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
.returning({ folderId: bookmarks.folderId });
if (deleted.length === 0) return;
const folderId = deleted[0].folderId;
const whereCondition = folderId != null
? and(eq(bookmarks.folderId, folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId))
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId));
const remaining = await tx
.select({ id: bookmarks.id, position: bookmarks.position })
.from(bookmarks)
.where(whereCondition)
.orderBy(bookmarks.position, bookmarks.id);
for (let idx = 0; idx < remaining.length; idx++) {
if (remaining[idx].position !== idx) {
await tx
.update(bookmarks)
.set({ position: idx, updatedAt: new Date() })
.where(and(eq(bookmarks.id, remaining[idx].id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
}
}
});
}
async normalizeBookmarkPositions(organizationId: number, userId: number): Promise<void> {
const allBookmarks = await db
.select()
.from(bookmarks)
.where(and(eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
.orderBy(bookmarks.folderId, bookmarks.position, bookmarks.id);
const groups = new Map<string, typeof allBookmarks>();
for (const bm of allBookmarks) {
const key = bm.folderId == null ? 'null' : String(bm.folderId);
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(bm);
}
const updates: { id: number; position: number }[] = [];
for (const group of groups.values()) {
group.forEach((bm, idx) => {
if (bm.position !== idx) {
updates.push({ id: bm.id, position: idx });
}
});
}
if (updates.length > 0) {
await this.batchUpdateBookmarkPositions(updates, organizationId, userId);
}
}
async batchUpdateBookmarkPositions(
items: { id: number; position: number }[],
organizationId: number,
userId: number
): Promise<void> {
await db.transaction(async (tx) => {
for (const item of items) {
await tx
.update(bookmarks)
.set({ position: item.position, updatedAt: new Date() })
.where(and(eq(bookmarks.id, item.id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
}
});
}
async batchUpdateBookmarkFolderPositions(
items: { id: number; position: number }[],
organizationId: number,
userId: number
): Promise<void> {
await db.transaction(async (tx) => {
for (const item of items) {
await tx
.update(bookmarkFolders)
.set({ position: item.position, updatedAt: new Date() })
.where(and(eq(bookmarkFolders.id, item.id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
}
});
}
async normalizeBookmarkFolderPositions(organizationId: number, userId: number): Promise<void> {
const allFolders = await db
.select()
.from(bookmarkFolders)
.where(and(eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
.orderBy(bookmarkFolders.parentId, bookmarkFolders.position, bookmarkFolders.id);
const groups = new Map<string, typeof allFolders>();
for (const folder of allFolders) {
const key = folder.parentId == null ? 'null' : String(folder.parentId);
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(folder);
}
const updates: { id: number; position: number }[] = [];
for (const group of groups.values()) {
group.forEach((folder, idx) => {
if (folder.position !== idx) {
updates.push({ id: folder.id, position: idx });
}
});
}
if (updates.length > 0) {
await this.batchUpdateBookmarkFolderPositions(updates, organizationId, userId);
}
}
// User Notifications
async getUserNotifications(userId: number, organizationId: number, isRead?: boolean): Promise<UserNotification[]> {
const conditions = [
eq(userNotifications.userId, userId),
eq(userNotifications.organizationId, organizationId)
];
if (isRead !== undefined) {
conditions.push(eq(userNotifications.isRead, isRead));
}
const result = await db.query.userNotifications.findMany({
where: and(...conditions),
orderBy: [desc(userNotifications.createdAt)],
with: {
task: true,
taskMessage: {
with: {
author: true
}
}
}
});
return result as UserNotification[];
}
async createUserNotification(insertNotification: InsertUserNotification): Promise<UserNotification> {
const [notification] = await db
.insert(userNotifications)
.values(insertNotification)
.returning();
return notification;
}
async markNotificationAsRead(id: number, userId: number, organizationId: number): Promise<UserNotification> {
const [notification] = await db
.update(userNotifications)
.set({ isRead: true })
.where(and(
eq(userNotifications.id, id),
eq(userNotifications.userId, userId),
eq(userNotifications.organizationId, organizationId)
))
.returning();
return notification;
}
async markAllNotificationsAsRead(userId: number, organizationId: number): Promise<void> {
await db
.update(userNotifications)
.set({ isRead: true })
.where(and(
eq(userNotifications.userId, userId),
eq(userNotifications.organizationId, organizationId)
));
}
async markTaskNotificationsAsRead(taskId: number, userId: number, organizationId: number): Promise<void> {
await db
.update(userNotifications)
.set({ isRead: true })
.where(and(
eq(userNotifications.taskId, taskId),
eq(userNotifications.userId, userId),
eq(userNotifications.organizationId, organizationId),
eq(userNotifications.isRead, false)
));
}
async deleteNotification(id: number, userId: number, organizationId: number): Promise<void> {
await db
.delete(userNotifications)
.where(and(
eq(userNotifications.id, id),
eq(userNotifications.userId, userId),
eq(userNotifications.organizationId, organizationId)
));
}
// User Search
async searchUsers(query: string, organizationId: number, limit = 10): Promise<User[]> {
// Поддержка похожих символов: і/и для корректного поиска
const normalizedQuery = query
.replace(/і/g, 'и')
.replace(/ї/g, 'и')
.trim();
const result = await db
.select()
.from(users)
.where(and(
eq(users.organizationId, organizationId),
sql`(
${users.firstName} || ' ' || ${users.middleName} || ' ' || ${users.lastName} ILIKE ${'%' + query + '%'}
OR
${users.firstName} || ' ' || ${users.middleName} || ' ' || ${users.lastName} ILIKE ${'%' + normalizedQuery + '%'}
OR
${users.firstName} ILIKE ${'%' + query + '%'}
OR
${users.middleName} ILIKE ${'%' + query + '%'}
OR
${users.lastName} ILIKE ${'%' + query + '%'}
)`
))
.limit(limit)
.orderBy(users.firstName, users.middleName, users.lastName);
return result;
}
// =====================================================
// BOTS
// =====================================================
}