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 { 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 { 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 { 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 { 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 { const [newTable] = await db .insert(dataTables) .values(table) .returning(); return newTable; } async updateDataTable(id: number, organizationId: number, updates: Partial): Promise { 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 { 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 { 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 { 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 { const [newRow] = await db .insert(dataTableRows) .values(row) .returning(); return newRow; } async updateDataTableRow(id: number, tableId: number, organizationId: number, updates: Partial): Promise { 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 { 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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 { return db .select() .from(dataTableAccessRules) .where(and( eq(dataTableAccessRules.tableId, tableId), eq(dataTableAccessRules.organizationId, organizationId) )); } async createDataTableAccessRule(data: InsertDataTableAccessRule): Promise { 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 { 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 }[]) || []; } }