fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
531
server/storage/data-tables-core.storage.ts
Normal file
531
server/storage/data-tables-core.storage.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { users, dataTables, dataTableRows, dataTablePermissions, dataTableAccessRules, formFields, forms, formAccessRules, roleMembers, type DataTable, type DataTableRow, type DataTablePermission, type InsertDataTable, type InsertDataTableRow, type InsertDataTablePermission, type DataTableFull, type DataTableAccessRule, type InsertDataTableAccessRule } from "@shared/schema";
|
||||
import { db } from "../db";
|
||||
import { eq, and, desc, sql, inArray } from "drizzle-orm";
|
||||
import { BotsStorage } from "./bots.storage";
|
||||
export class DataTablesCoreStorage extends BotsStorage {
|
||||
async getDataTable(id: number, organizationId: number): Promise<DataTable | undefined> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, id),
|
||||
eq(dataTables.organizationId, organizationId),
|
||||
eq(dataTables.isDeleted, false)
|
||||
));
|
||||
return table || undefined;
|
||||
}
|
||||
|
||||
async getDataTablesByOrganization(organizationId: number): Promise<DataTable[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.organizationId, organizationId),
|
||||
eq(dataTables.isDeleted, false)
|
||||
))
|
||||
.orderBy(desc(dataTables.createdAt));
|
||||
}
|
||||
|
||||
async getDataTablesWithAccess(userId: number, organizationId: number): Promise<DataTable[]> {
|
||||
const allTables = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.organizationId, organizationId),
|
||||
eq(dataTables.isDeleted, false)
|
||||
))
|
||||
.orderBy(desc(dataTables.createdAt));
|
||||
|
||||
const tablesWithAccess: DataTable[] = [];
|
||||
for (const table of allTables) {
|
||||
if (table.createdBy === userId) {
|
||||
tablesWithAccess.push(table);
|
||||
} else {
|
||||
const [permission] = await db
|
||||
.select()
|
||||
.from(dataTablePermissions)
|
||||
.where(and(
|
||||
eq(dataTablePermissions.tableId, table.id),
|
||||
eq(dataTablePermissions.userId, userId)
|
||||
));
|
||||
if (permission) {
|
||||
tablesWithAccess.push(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tablesWithAccess;
|
||||
}
|
||||
|
||||
async getDataTableFull(id: number, organizationId: number): Promise<DataTableFull | undefined> {
|
||||
const table = await this.getDataTable(id, organizationId);
|
||||
if (!table) return undefined;
|
||||
|
||||
const rows = await this.getDataTableRows(id, organizationId);
|
||||
const permissions = await this.getDataTablePermissions(id, organizationId);
|
||||
|
||||
return {
|
||||
...table,
|
||||
rows,
|
||||
permissions,
|
||||
};
|
||||
}
|
||||
|
||||
async createDataTable(table: InsertDataTable & { organizationId: number; createdBy: number }): Promise<DataTable> {
|
||||
const [newTable] = await db
|
||||
.insert(dataTables)
|
||||
.values(table)
|
||||
.returning();
|
||||
return newTable;
|
||||
}
|
||||
|
||||
async updateDataTable(id: number, organizationId: number, updates: Partial<DataTable>): Promise<DataTable> {
|
||||
const [table] = await db
|
||||
.update(dataTables)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTables.id, id),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
))
|
||||
.returning();
|
||||
return table;
|
||||
}
|
||||
|
||||
async deleteDataTable(id: number, organizationId: number): Promise<void> {
|
||||
await db
|
||||
.update(dataTables)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTables.id, id),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// DATA TABLE ROWS
|
||||
// =====================================================
|
||||
|
||||
async getDataTableRows(tableId: number, organizationId: number): Promise<DataTableRow[]> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) return [];
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(dataTableRows)
|
||||
.where(and(
|
||||
eq(dataTableRows.tableId, tableId),
|
||||
eq(dataTableRows.isDeleted, false)
|
||||
))
|
||||
.orderBy(dataTableRows.position, dataTableRows.id);
|
||||
}
|
||||
|
||||
async getDataTableRow(id: number, tableId: number, organizationId: number): Promise<DataTableRow | undefined> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) return undefined;
|
||||
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(dataTableRows)
|
||||
.where(and(
|
||||
eq(dataTableRows.id, id),
|
||||
eq(dataTableRows.tableId, tableId),
|
||||
eq(dataTableRows.isDeleted, false)
|
||||
));
|
||||
return row || undefined;
|
||||
}
|
||||
|
||||
async createDataTableRow(row: InsertDataTableRow): Promise<DataTableRow> {
|
||||
const [newRow] = await db
|
||||
.insert(dataTableRows)
|
||||
.values(row)
|
||||
.returning();
|
||||
return newRow;
|
||||
}
|
||||
|
||||
async updateDataTableRow(id: number, tableId: number, organizationId: number, updates: Partial<DataTableRow>): Promise<DataTableRow> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) throw new Error('Table not found');
|
||||
|
||||
const [row] = await db
|
||||
.update(dataTableRows)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTableRows.id, id),
|
||||
eq(dataTableRows.tableId, tableId)
|
||||
))
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
async deleteDataTableRow(id: number, tableId: number, organizationId: number): Promise<void> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) return;
|
||||
|
||||
await db
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(dataTableRows.id, id),
|
||||
eq(dataTableRows.tableId, tableId)
|
||||
));
|
||||
}
|
||||
|
||||
async bulkCreateDataTableRows(tableId: number, rows: { values: string[] }[]): Promise<DataTableRow[]> {
|
||||
if (rows.length === 0) return [];
|
||||
|
||||
const insertData = rows.map(row => ({
|
||||
tableId,
|
||||
values: row.values,
|
||||
}));
|
||||
|
||||
return await db
|
||||
.insert(dataTableRows)
|
||||
.values(insertData)
|
||||
.returning();
|
||||
}
|
||||
|
||||
async bulkDeleteDataTableRows(tableId: number, organizationId: number): Promise<void> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) return;
|
||||
|
||||
await db
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(eq(dataTableRows.tableId, tableId));
|
||||
}
|
||||
|
||||
async applyDataTableSync(
|
||||
tableId: number,
|
||||
deleted: { id: number }[],
|
||||
added: { values: string[]; position: number }[],
|
||||
reordered: { id: number; newPosition: number }[],
|
||||
positionUpdates: { id: number; position: number }[]
|
||||
): Promise<void> {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const row of deleted) {
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(eq(dataTableRows.id, row.id), eq(dataTableRows.tableId, tableId)));
|
||||
}
|
||||
for (const row of added) {
|
||||
await tx
|
||||
.insert(dataTableRows)
|
||||
.values({ tableId, values: row.values, position: row.position });
|
||||
}
|
||||
for (const row of reordered) {
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ position: row.newPosition, updatedAt: new Date() })
|
||||
.where(and(eq(dataTableRows.id, row.id), eq(dataTableRows.tableId, tableId)));
|
||||
}
|
||||
for (const row of positionUpdates) {
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ position: row.position, updatedAt: new Date() })
|
||||
.where(and(eq(dataTableRows.id, row.id), eq(dataTableRows.tableId, tableId)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async applyDataTableDiff(
|
||||
tableId: number,
|
||||
upsertRows: { values: string[] }[],
|
||||
deleteKeys: string[],
|
||||
currentRows: DataTableRow[]
|
||||
): Promise<{ added: DataTableRow[]; updated: DataTableRow[]; deleted: DataTableRow[] }> {
|
||||
const added: DataTableRow[] = [];
|
||||
const updated: DataTableRow[] = [];
|
||||
const deleted: DataTableRow[] = [];
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const currentKeyMap = new Map(currentRows.map(r => [r.values?.[0] ?? '', r]));
|
||||
|
||||
for (const row of upsertRows) {
|
||||
const key = row.values[0] ?? '';
|
||||
const existing = currentKeyMap.get(key);
|
||||
if (existing) {
|
||||
const [updatedRow] = await tx
|
||||
.update(dataTableRows)
|
||||
.set({ values: row.values, updatedAt: new Date() })
|
||||
.where(and(eq(dataTableRows.id, existing.id), eq(dataTableRows.tableId, tableId)))
|
||||
.returning();
|
||||
if (updatedRow) updated.push(updatedRow);
|
||||
} else {
|
||||
const [newRow] = await tx
|
||||
.insert(dataTableRows)
|
||||
.values({ tableId, values: row.values })
|
||||
.returning();
|
||||
added.push(newRow);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of deleteKeys) {
|
||||
const existing = currentKeyMap.get(key);
|
||||
if (existing) {
|
||||
await tx
|
||||
.update(dataTableRows)
|
||||
.set({ isDeleted: true, updatedAt: new Date() })
|
||||
.where(and(eq(dataTableRows.id, existing.id), eq(dataTableRows.tableId, tableId)));
|
||||
deleted.push(existing);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { added, updated, deleted };
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// DATA TABLE PERMISSIONS
|
||||
// =====================================================
|
||||
|
||||
async getDataTablePermissions(tableId: number, organizationId: number): Promise<(DataTablePermission & { user: { id: number; firstName: string; lastName: string; email: string } })[]> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(dataTables)
|
||||
.where(and(
|
||||
eq(dataTables.id, tableId),
|
||||
eq(dataTables.organizationId, organizationId)
|
||||
));
|
||||
if (!table) return [];
|
||||
|
||||
const permissions = await db
|
||||
.select({
|
||||
id: dataTablePermissions.id,
|
||||
tableId: dataTablePermissions.tableId,
|
||||
userId: dataTablePermissions.userId,
|
||||
role: dataTablePermissions.role,
|
||||
createdAt: dataTablePermissions.createdAt,
|
||||
user: {
|
||||
id: users.id,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
email: users.email,
|
||||
}
|
||||
})
|
||||
.from(dataTablePermissions)
|
||||
.innerJoin(users, eq(dataTablePermissions.userId, users.id))
|
||||
.where(eq(dataTablePermissions.tableId, tableId));
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
async getDataTablePermission(tableId: number, userId: number): Promise<DataTablePermission | undefined> {
|
||||
const [permission] = await db
|
||||
.select()
|
||||
.from(dataTablePermissions)
|
||||
.where(and(
|
||||
eq(dataTablePermissions.tableId, tableId),
|
||||
eq(dataTablePermissions.userId, userId)
|
||||
));
|
||||
return permission || undefined;
|
||||
}
|
||||
|
||||
async resolveDataTableAccessRules(tableId: number, userId: number, organizationId: number): Promise<'admin' | 'editor' | 'reader' | null> {
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(dataTableAccessRules)
|
||||
.where(and(
|
||||
eq(dataTableAccessRules.tableId, tableId),
|
||||
eq(dataTableAccessRules.organizationId, organizationId)
|
||||
));
|
||||
|
||||
if (rules.length === 0) return null;
|
||||
|
||||
const levelRank: Record<string, number> = { reader: 1, editor: 2, admin: 3 };
|
||||
let bestLevel: string | null = null;
|
||||
let bestRank = 0;
|
||||
|
||||
for (const rule of rules) {
|
||||
let matches = false;
|
||||
if (rule.targetType === 'user' && rule.targetId === userId) {
|
||||
matches = true;
|
||||
} else if (rule.targetType === 'role') {
|
||||
const member = await db
|
||||
.select()
|
||||
.from(roleMembers)
|
||||
.where(and(eq(roleMembers.userId, userId), eq(roleMembers.roleId, rule.targetId)))
|
||||
.limit(1);
|
||||
if (member.length > 0) matches = true;
|
||||
}
|
||||
if (matches) {
|
||||
const rank = levelRank[rule.accessLevel] ?? 0;
|
||||
if (rank > bestRank) {
|
||||
bestRank = rank;
|
||||
bestLevel = rule.accessLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestLevel as 'admin' | 'editor' | 'reader' | null;
|
||||
}
|
||||
|
||||
async hasAccessToFormsUsingTable(tableId: number, userId: number, organizationId: number): Promise<boolean> {
|
||||
const result = await db.execute(sql`
|
||||
WITH linked_forms AS (
|
||||
SELECT DISTINCT form_id
|
||||
FROM form_fields
|
||||
WHERE type = 'table'
|
||||
AND (options->>'tableId')::int = ${tableId}
|
||||
)
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM linked_forms lf
|
||||
JOIN forms f ON f.id = lf.form_id
|
||||
WHERE f.organization_id = ${organizationId}
|
||||
AND (
|
||||
f.visibility = 'organization'
|
||||
OR f.created_by = ${userId}
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM form_access_rules far
|
||||
WHERE far.form_id = f.id
|
||||
AND far.organization_id = ${organizationId}
|
||||
AND far.access_level IN ('participate', 'view_all', 'admin')
|
||||
AND (
|
||||
(far.target_type = 'user' AND far.target_id = ${userId})
|
||||
OR (
|
||||
far.target_type = 'role'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM role_members rm
|
||||
WHERE rm.role_id = far.target_id AND rm.user_id = ${userId}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) AS has_access
|
||||
`);
|
||||
const row = (result.rows as { has_access: boolean }[])[0];
|
||||
return row?.has_access ?? false;
|
||||
}
|
||||
|
||||
async getUserTableRole(tableId: number, userId: number, createdBy: number, organizationId?: number): Promise<'admin' | 'editor' | 'reader' | null> {
|
||||
if (userId === createdBy) return 'admin';
|
||||
|
||||
if (organizationId) {
|
||||
const table = await this.getDataTable(tableId, organizationId);
|
||||
if (table?.visibility === 'organization') return 'reader';
|
||||
}
|
||||
|
||||
const permission = await this.getDataTablePermission(tableId, userId);
|
||||
if (permission) return permission.role as 'admin' | 'editor' | 'reader';
|
||||
|
||||
if (organizationId) {
|
||||
const ruleAccess = await this.resolveDataTableAccessRules(tableId, userId, organizationId);
|
||||
if (ruleAccess) return ruleAccess;
|
||||
|
||||
const hasFormAccess = await this.hasAccessToFormsUsingTable(tableId, userId, organizationId);
|
||||
if (hasFormAccess) return 'reader';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async setDataTablePermission(permission: InsertDataTablePermission): Promise<DataTablePermission> {
|
||||
const [existingPermission] = await db
|
||||
.select()
|
||||
.from(dataTablePermissions)
|
||||
.where(and(
|
||||
eq(dataTablePermissions.tableId, permission.tableId),
|
||||
eq(dataTablePermissions.userId, permission.userId)
|
||||
));
|
||||
|
||||
if (existingPermission) {
|
||||
const [updated] = await db
|
||||
.update(dataTablePermissions)
|
||||
.set({ role: permission.role })
|
||||
.where(eq(dataTablePermissions.id, existingPermission.id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
const [newPermission] = await db
|
||||
.insert(dataTablePermissions)
|
||||
.values(permission)
|
||||
.returning();
|
||||
return newPermission;
|
||||
}
|
||||
|
||||
async deleteDataTablePermission(tableId: number, userId: number): Promise<void> {
|
||||
await db
|
||||
.delete(dataTablePermissions)
|
||||
.where(and(
|
||||
eq(dataTablePermissions.tableId, tableId),
|
||||
eq(dataTablePermissions.userId, userId)
|
||||
));
|
||||
}
|
||||
|
||||
// Data table access rules (role-based)
|
||||
async getDataTableAccessRules(tableId: number, organizationId: number): Promise<DataTableAccessRule[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(dataTableAccessRules)
|
||||
.where(and(
|
||||
eq(dataTableAccessRules.tableId, tableId),
|
||||
eq(dataTableAccessRules.organizationId, organizationId)
|
||||
));
|
||||
}
|
||||
|
||||
async createDataTableAccessRule(data: InsertDataTableAccessRule): Promise<DataTableAccessRule> {
|
||||
const [rule] = await db
|
||||
.insert(dataTableAccessRules)
|
||||
.values(data)
|
||||
.onConflictDoUpdate({
|
||||
target: [dataTableAccessRules.tableId, dataTableAccessRules.targetType, dataTableAccessRules.targetId],
|
||||
set: { accessLevel: data.accessLevel },
|
||||
})
|
||||
.returning();
|
||||
return rule;
|
||||
}
|
||||
|
||||
async deleteDataTableAccessRule(id: number, organizationId: number): Promise<void> {
|
||||
await db
|
||||
.delete(dataTableAccessRules)
|
||||
.where(and(
|
||||
eq(dataTableAccessRules.id, id),
|
||||
eq(dataTableAccessRules.organizationId, organizationId)
|
||||
));
|
||||
}
|
||||
|
||||
async getLinkedFormsForTable(tableId: number, organizationId: number): Promise<{ id: number; name: string }[]> {
|
||||
const result = await db.execute(sql`
|
||||
SELECT DISTINCT f.id, f.name
|
||||
FROM form_fields ff
|
||||
JOIN forms f ON f.id = ff.form_id
|
||||
WHERE ff.type = 'table'
|
||||
AND (ff.options->>'tableId')::int = ${tableId}
|
||||
AND f.organization_id = ${organizationId}
|
||||
`);
|
||||
return (result.rows as { id: number; name: string }[]) || [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user