- 15 инструментов: list/get_directory, list_directory_rows (flat/tree), get_directory_row, get_directory_column_values (read); create/bulk_create_directory_row (write); create/update/delete_directory, update/delete/move_directory_row, link/unlink_directories (full) - Объектные проверки isTableAllowed по scopes.tableIds - Логика дерева строк вынесена в server/utils/data-table-tree.ts (переиспользуется REST-эндпоинтом и MCP) - bulkCreateDataTableRows: поддержка parentId и position
723 lines
24 KiB
TypeScript
723 lines
24 KiB
TypeScript
import { users, dataTables, dataTableRows, dataTablePermissions, dataTableAccessRules, dataTableLinks, formFields, forms, formAccessRules, roleMembers, type DataTable, type DataTableRow, type DataTablePermission, type InsertDataTable, type InsertDataTableRow, type InsertDataTablePermission, type DataTableFull, type DataTableAccessRule, type InsertDataTableAccessRule, type DataTableLink, type DataTableSubdirectory } 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;
|
|
}
|
|
|
|
// =====================================================
|
|
// DATA TABLE LINKS (подсправочники)
|
|
// =====================================================
|
|
|
|
async getDataTableLinks(parentTableId: number, organizationId: number): Promise<DataTableSubdirectory[]> {
|
|
const links = await db
|
|
.select({
|
|
id: dataTableLinks.id,
|
|
organizationId: dataTableLinks.organizationId,
|
|
parentTableId: dataTableLinks.parentTableId,
|
|
childTableId: dataTableLinks.childTableId,
|
|
parentColumnIndex: dataTableLinks.parentColumnIndex,
|
|
childColumnIndex: dataTableLinks.childColumnIndex,
|
|
parentRowIds: dataTableLinks.parentRowIds,
|
|
createdAt: dataTableLinks.createdAt,
|
|
updatedAt: dataTableLinks.updatedAt,
|
|
childTable: {
|
|
id: dataTables.id,
|
|
name: dataTables.name,
|
|
columns: dataTables.columns,
|
|
},
|
|
})
|
|
.from(dataTableLinks)
|
|
.innerJoin(dataTables, eq(dataTableLinks.childTableId, dataTables.id))
|
|
.where(and(
|
|
eq(dataTableLinks.parentTableId, parentTableId),
|
|
eq(dataTableLinks.organizationId, organizationId),
|
|
eq(dataTables.isDeleted, false)
|
|
))
|
|
.orderBy(dataTableLinks.id);
|
|
|
|
return links as DataTableSubdirectory[];
|
|
}
|
|
|
|
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);
|
|
const subdirectories = await this.getDataTableLinks(id, organizationId);
|
|
|
|
return {
|
|
...table,
|
|
rows,
|
|
permissions,
|
|
subdirectories,
|
|
};
|
|
}
|
|
|
|
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.transaction(async (tx) => {
|
|
// Move direct children to root before soft-deleting the row
|
|
await tx
|
|
.update(dataTableRows)
|
|
.set({ parentId: null, updatedAt: new Date() })
|
|
.where(and(
|
|
eq(dataTableRows.parentId, id),
|
|
eq(dataTableRows.tableId, tableId)
|
|
));
|
|
|
|
await tx
|
|
.update(dataTableRows)
|
|
.set({ isDeleted: true, updatedAt: new Date() })
|
|
.where(and(
|
|
eq(dataTableRows.id, id),
|
|
eq(dataTableRows.tableId, tableId)
|
|
));
|
|
});
|
|
}
|
|
|
|
private async getRowsMap(tableId: number, organizationId: number): Promise<Map<number, DataTableRow>> {
|
|
const rows = await this.getDataTableRows(tableId, organizationId);
|
|
const map = new Map<number, DataTableRow>();
|
|
for (const row of rows) {
|
|
map.set(row.id, row);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private getDescendantIds(rowsById: Map<number, DataTableRow>, rootId: number): Set<number> {
|
|
const ids = new Set<number>();
|
|
const walk = (parentId: number) => {
|
|
for (const row of rowsById.values()) {
|
|
if (row.parentId === parentId) {
|
|
ids.add(row.id);
|
|
walk(row.id);
|
|
}
|
|
}
|
|
};
|
|
walk(rootId);
|
|
return ids;
|
|
}
|
|
|
|
async moveDataTableRow(
|
|
id: number,
|
|
tableId: number,
|
|
organizationId: number,
|
|
parentId: number | null,
|
|
position: number
|
|
): Promise<DataTableRow> {
|
|
const table = await this.getDataTable(tableId, organizationId);
|
|
if (!table) throw new Error('Table not found');
|
|
|
|
const row = await this.getDataTableRow(id, tableId, organizationId);
|
|
if (!row) throw new Error('Row not found');
|
|
|
|
const targetParentId = parentId ?? null;
|
|
|
|
if (targetParentId !== null) {
|
|
if (targetParentId === id) {
|
|
throw new Error('Cannot move row into itself');
|
|
}
|
|
const rowsById = await this.getRowsMap(tableId, organizationId);
|
|
if (this.getDescendantIds(rowsById, id).has(targetParentId)) {
|
|
throw new Error('Cannot move row into its descendant');
|
|
}
|
|
}
|
|
|
|
const rows = await this.getDataTableRows(tableId, organizationId);
|
|
const siblings = rows
|
|
.filter((r) => r.id !== id && (r.parentId ?? null) === targetParentId)
|
|
.sort((a, b) => a.position - b.position);
|
|
|
|
const clampedPosition = Math.max(0, Math.min(position, siblings.length));
|
|
const reordered = [
|
|
...siblings.slice(0, clampedPosition),
|
|
row,
|
|
...siblings.slice(clampedPosition),
|
|
];
|
|
|
|
await db.transaction(async (tx) => {
|
|
for (let i = 0; i < reordered.length; i++) {
|
|
const updates: Partial<DataTableRow> = { position: i };
|
|
if (reordered[i].id === id) {
|
|
updates.parentId = targetParentId;
|
|
}
|
|
await tx
|
|
.update(dataTableRows)
|
|
.set({ ...updates, updatedAt: new Date() })
|
|
.where(eq(dataTableRows.id, reordered[i].id));
|
|
}
|
|
});
|
|
|
|
const movedRow = await this.getDataTableRow(id, tableId, organizationId);
|
|
if (!movedRow) throw new Error('Row not found after move');
|
|
return movedRow;
|
|
}
|
|
|
|
async bulkCreateDataTableRows(tableId: number, rows: { values: string[]; parentId?: number | null; position?: number }[]): Promise<DataTableRow[]> {
|
|
if (rows.length === 0) return [];
|
|
|
|
const insertData = rows.map(row => ({
|
|
tableId,
|
|
values: row.values,
|
|
parentId: row.parentId ?? null,
|
|
position: row.position ?? 0,
|
|
}));
|
|
|
|
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));
|
|
}
|
|
|
|
// =====================================================
|
|
// DATA TABLE LINKS (подсправочники)
|
|
// =====================================================
|
|
|
|
async getDataTableLink(id: number, organizationId: number): Promise<DataTableLink | undefined> {
|
|
const [link] = await db
|
|
.select()
|
|
.from(dataTableLinks)
|
|
.where(and(
|
|
eq(dataTableLinks.id, id),
|
|
eq(dataTableLinks.organizationId, organizationId)
|
|
));
|
|
return link || undefined;
|
|
}
|
|
|
|
async createDataTableLink(data: { organizationId: number; parentTableId: number; childTableId: number; parentColumnIndex?: number; childColumnIndex?: number; parentRowIds?: number[] }): Promise<DataTableLink> {
|
|
const [link] = await db
|
|
.insert(dataTableLinks)
|
|
.values({
|
|
organizationId: data.organizationId,
|
|
parentTableId: data.parentTableId,
|
|
childTableId: data.childTableId,
|
|
parentColumnIndex: data.parentColumnIndex ?? 0,
|
|
childColumnIndex: data.childColumnIndex ?? 0,
|
|
parentRowIds: data.parentRowIds ?? [],
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [dataTableLinks.parentTableId, dataTableLinks.childTableId],
|
|
set: {
|
|
parentColumnIndex: data.parentColumnIndex ?? 0,
|
|
childColumnIndex: data.childColumnIndex ?? 0,
|
|
parentRowIds: data.parentRowIds ?? [],
|
|
updatedAt: new Date(),
|
|
},
|
|
})
|
|
.returning();
|
|
return link;
|
|
}
|
|
|
|
async updateDataTableLink(id: number, organizationId: number, updates: Partial<DataTableLink>): Promise<DataTableLink> {
|
|
const [link] = await db
|
|
.update(dataTableLinks)
|
|
.set({ ...updates, updatedAt: new Date() })
|
|
.where(and(
|
|
eq(dataTableLinks.id, id),
|
|
eq(dataTableLinks.organizationId, organizationId)
|
|
))
|
|
.returning();
|
|
return link;
|
|
}
|
|
|
|
async deleteDataTableLink(id: number, organizationId: number): Promise<void> {
|
|
await db
|
|
.delete(dataTableLinks)
|
|
.where(and(
|
|
eq(dataTableLinks.id, id),
|
|
eq(dataTableLinks.organizationId, organizationId)
|
|
));
|
|
}
|
|
|
|
async applyDataTableSync(
|
|
tableId: number,
|
|
deleted: { id: number }[],
|
|
added: { values: string[]; position: number; parentId?: number | null }[],
|
|
reordered: { id: number; newPosition: number; parentId?: number | null }[],
|
|
positionUpdates: { id: number; position: number; parentId?: number | null }[]
|
|
): 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, parentId: row.parentId ?? null });
|
|
}
|
|
for (const row of reordered) {
|
|
const updates: Partial<typeof dataTableRows.$inferInsert> = { position: row.newPosition, updatedAt: new Date() };
|
|
if (row.parentId !== undefined) updates.parentId = row.parentId;
|
|
await tx
|
|
.update(dataTableRows)
|
|
.set(updates)
|
|
.where(and(eq(dataTableRows.id, row.id), eq(dataTableRows.tableId, tableId)));
|
|
}
|
|
for (const row of positionUpdates) {
|
|
const updates: Partial<typeof dataTableRows.$inferInsert> = { position: row.position, updatedAt: new Date() };
|
|
if (row.parentId !== undefined) updates.parentId = row.parentId;
|
|
await tx
|
|
.update(dataTableRows)
|
|
.set(updates)
|
|
.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 }[]) || [];
|
|
}
|
|
}
|