Files
iistwin/server/storage/forms.storage.ts

515 lines
18 KiB
TypeScript

import { forms, formTabs, formFields, formStatuses, statusTransitions, tasks, bookmarks, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type InsertForm, type InsertFormTab, type InsertFormField, type InsertFormStatus, type InsertStatusTransition } from "@shared/schema";
import { db } from "../db";
import { eq, and, desc, exists, sql } from "drizzle-orm";
import crypto from "crypto";
import { UserProfileStorage } from "./user-profile.storage";
export class FormsStorage extends UserProfileStorage {
// Forms
async getForm(id: number, organizationId: number): Promise<Form | undefined> {
const [form] = await db
.select()
.from(forms)
.where(and(eq(forms.id, id), eq(forms.organizationId, organizationId)));
return form || undefined;
}
async getFormsByOrganization(organizationId: number): Promise<Form[]> {
return await db
.select()
.from(forms)
.where(eq(forms.organizationId, organizationId))
.orderBy(desc(forms.createdAt));
}
async createForm(insertForm: InsertForm): Promise<Form> {
const [form] = await db
.insert(forms)
.values(insertForm)
.returning();
return form;
}
async updateForm(id: number, organizationId: number, updates: Partial<Form>): Promise<Form> {
const [form] = await db
.update(forms)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(forms.id, id), eq(forms.organizationId, organizationId)))
.returning();
return form;
}
async deleteForm(id: number, organizationId: number): Promise<void> {
// Cleanup RAG embeddings for this form and all its tasks/messages
const { deleteRagEmbeddingsByForm } = await import('../services/embedding.service.js');
await deleteRagEmbeddingsByForm(organizationId, id);
// Удаляем закладки, ссылающиеся на эту форму
await db
.delete(bookmarks)
.where(eq(bookmarks.formId, id));
// Удаляем все задачи формы (каскадно удалятся их сообщения, значения полей и т.д.)
await db
.delete(tasks)
.where(eq(tasks.formId, id));
// Теперь удаляем саму форму
await db
.delete(forms)
.where(and(eq(forms.id, id), eq(forms.organizationId, organizationId)));
}
async getTasksCountByForm(formId: number): Promise<{ total: number; active: number; completed: number }> {
const result = await db
.select({
total: sql<number>`count(*)::int`,
active: sql<number>`count(*) filter (where is_completed = false)::int`,
completed: sql<number>`count(*) filter (where is_completed = true)::int`
})
.from(tasks)
.where(eq(tasks.formId, formId));
return {
total: result[0]?.total || 0,
active: result[0]?.active || 0,
completed: result[0]?.completed || 0
};
}
// Form Tabs
async getFormTabs(formId: number, organizationId: number): Promise<FormTab[]> {
return await db
.select()
.from(formTabs)
.innerJoin(forms, eq(formTabs.formId, forms.id))
.where(and(eq(formTabs.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(formTabs.displayOrder)
.then(results => results.map(r => r.form_tabs));
}
async getFormTabsByCreationOrder(formId: number, organizationId: number): Promise<FormTab[]> {
return await db
.select()
.from(formTabs)
.innerJoin(forms, eq(formTabs.formId, forms.id))
.where(and(eq(formTabs.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(formTabs.id)
.then(results => results.map(r => r.form_tabs));
}
async getFormTab(id: number, formId: number, organizationId: number): Promise<FormTab | undefined> {
const results = await db
.select()
.from(formTabs)
.innerJoin(forms, eq(formTabs.formId, forms.id))
.where(and(eq(formTabs.id, id), eq(formTabs.formId, formId), eq(forms.organizationId, organizationId)));
return results[0]?.form_tabs;
}
async createFormTab(insertTab: InsertFormTab): Promise<FormTab> {
const [tab] = await db
.insert(formTabs)
.values(insertTab)
.returning();
return tab;
}
async updateFormTab(id: number, formId: number, organizationId: number, updates: Partial<FormTab>): Promise<FormTab> {
const [tab] = await db
.update(formTabs)
.set(updates)
.where(and(
eq(formTabs.id, id),
eq(formTabs.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
return tab;
}
async deleteFormTab(id: number, formId: number, organizationId: number): Promise<void> {
await db
.delete(formTabs)
.where(and(
eq(formTabs.id, id),
eq(formTabs.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
));
}
async bulkSaveFormTabs(
formId: number,
organizationId: number,
tabs: Array<{ id?: number; name: string; code: string; type: string; position?: number; displayOrder?: number; tableColumns?: unknown }>
): Promise<FormTab[]> {
return await db.transaction(async (tx) => {
const existingTabs = await tx
.select()
.from(formTabs)
.where(eq(formTabs.formId, formId));
const incomingIds = tabs.filter(t => t.id && t.id > 0).map(t => t.id!);
const tabsToDelete = existingTabs.filter(e => !incomingIds.includes(e.id));
for (const tab of tabsToDelete) {
await tx.delete(formTabs).where(and(eq(formTabs.id, tab.id), eq(formTabs.formId, formId)));
}
const savedTabs: FormTab[] = [];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
const tabData = {
formId,
name: tab.name || '',
code: tab.code || `tab_${i + 1}`,
type: tab.type || 'fields',
position: tab.position ?? i,
displayOrder: tab.displayOrder ?? i,
tableColumns: tab.tableColumns || null,
};
if (tab.id && tab.id > 0) {
const [updated] = await tx
.update(formTabs)
.set(tabData)
.where(and(eq(formTabs.id, tab.id), eq(formTabs.formId, formId)))
.returning();
if (updated) savedTabs.push(updated);
} else {
const [created] = await tx
.insert(formTabs)
.values(tabData)
.returning();
savedTabs.push(created);
}
}
return savedTabs;
});
}
// Form Fields
async getFormFields(formId: number, organizationId: number): Promise<FormField[]> {
return await db
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(formFields.displayOrder)
.then(results => results.map(r => r.form_fields));
}
async getFormFieldsByCreationOrder(formId: number, organizationId: number): Promise<FormField[]> {
return await db
.select()
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(formFields.id)
.then(results => results.map(r => r.form_fields));
}
async getFormFieldById(id: number, organizationId: number): Promise<FormField | undefined> {
const [field] = await db
.select({ field: formFields })
.from(formFields)
.innerJoin(forms, eq(formFields.formId, forms.id))
.where(and(eq(formFields.id, id), eq(forms.organizationId, organizationId)));
return field?.field || undefined;
}
async createFormField(insertField: InsertFormField): Promise<FormField> {
const [field] = await db
.insert(formFields)
.values(insertField)
.returning();
return field;
}
async updateFormField(id: number, formId: number, organizationId: number, updates: Partial<FormField>): Promise<FormField> {
const [field] = await db
.update(formFields)
.set(updates)
.where(and(
eq(formFields.id, id),
eq(formFields.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
return field;
}
async deleteFormField(id: number, formId: number, organizationId: number): Promise<void> {
await db
.delete(formFields)
.where(and(
eq(formFields.id, id),
eq(formFields.formId, formId),
// Убедимся что form принадлежит организации через subquery
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
));
}
async bulkSaveFormFields(
formId: number,
organizationId: number,
fieldIdsToDelete: number[],
fields: Array<{
id?: number;
name: string;
code: string;
type: string;
isRequired?: boolean;
placeholder?: string | null;
defaultValue?: string | null;
validationRules?: unknown;
requiredRules?: unknown;
immutableRules?: unknown;
visibilityRules?: unknown;
options?: unknown;
position?: number | null;
displayOrder?: number | null;
tabId?: number | null;
linkedFormId?: number | null;
taskRelationType?: string | null;
buttonActionType?: string | null;
buttonUrl?: string | null;
buttonFormId?: number | null;
companyAutofill?: unknown;
maxFileCount?: number | null;
maxFileSizeMB?: number | null;
autoProlongation?: boolean | null;
autoProlongationLabel?: string | null;
linkToUserProfile?: boolean | null;
allowMultiple?: boolean | null;
ganttRole?: string | null;
}>
): Promise<FormField[]> {
return await db.transaction(async (tx) => {
for (const fieldId of fieldIdsToDelete) {
await tx
.delete(formFields)
.where(and(eq(formFields.id, fieldId), eq(formFields.formId, formId)));
}
const resultFields: FormField[] = [];
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const fieldData = {
formId,
name: field.name,
code: field.code,
type: field.type,
isRequired: field.isRequired || false,
placeholder: field.placeholder || null,
defaultValue: field.defaultValue || null,
validationRules: field.validationRules || null,
requiredRules: field.requiredRules || null,
immutableRules: field.immutableRules || null,
visibilityRules: field.visibilityRules || null,
options: field.options || null,
position: field.position ?? i,
displayOrder: field.displayOrder ?? i,
tabId: field.tabId || null,
linkedFormId: field.linkedFormId || null,
taskRelationType: field.taskRelationType || null,
buttonActionType: field.buttonActionType || null,
buttonUrl: field.buttonUrl || null,
buttonFormId: field.buttonFormId || null,
companyAutofill: field.companyAutofill || null,
maxFileCount: field.maxFileCount != null && field.maxFileCount > 0 ? field.maxFileCount : null,
maxFileSizeMB: field.maxFileSizeMB != null && field.maxFileSizeMB > 0 ? field.maxFileSizeMB : null,
autoProlongation: field.autoProlongation || false,
autoProlongationLabel: field.autoProlongationLabel || null,
linkToUserProfile: field.linkToUserProfile || false,
allowMultiple: field.allowMultiple || false,
ganttRole: field.ganttRole || null,
};
if (field.id && field.id > 0) {
const [updated] = await tx
.update(formFields)
.set(fieldData)
.where(and(
eq(formFields.id, field.id),
eq(formFields.formId, formId),
exists(tx.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
if (updated) resultFields.push(updated);
} else {
const [created] = await tx
.insert(formFields)
.values(fieldData)
.returning();
resultFields.push(created);
}
}
return resultFields;
});
}
// Form Statuses
async getFormStatuses(formId: number, organizationId: number): Promise<FormStatus[]> {
return await db
.select()
.from(formStatuses)
.innerJoin(forms, eq(formStatuses.formId, forms.id))
.where(and(eq(formStatuses.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(formStatuses.position)
.then(results => results.map(r => r.form_statuses));
}
async createFormStatus(insertStatus: InsertFormStatus): Promise<FormStatus> {
const [status] = await db
.insert(formStatuses)
.values(insertStatus)
.returning();
return status;
}
async updateFormStatus(id: number, formId: number, organizationId: number, updates: Partial<FormStatus>): Promise<FormStatus> {
const [status] = await db
.update(formStatuses)
.set(updates)
.where(and(
eq(formStatuses.id, id),
eq(formStatuses.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
return status;
}
async deleteFormStatus(id: number, formId: number, organizationId: number): Promise<void> {
await db
.delete(formStatuses)
.where(and(
eq(formStatuses.id, id),
eq(formStatuses.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
));
}
async bulkSaveFormStatuses(
formId: number,
organizationId: number,
statusIdsToDelete: number[],
statuses: Array<{ id?: number; name: string; color?: string; isInitial?: boolean; isFinal?: boolean; entryConditions?: { fieldCode: string; value: string }[] | null }>
): Promise<FormStatus[]> {
return await db.transaction(async (tx) => {
for (const statusId of statusIdsToDelete) {
await tx
.delete(formStatuses)
.where(and(eq(formStatuses.id, statusId), eq(formStatuses.formId, formId)));
}
const resultStatuses: FormStatus[] = [];
for (let i = 0; i < statuses.length; i++) {
const status = statuses[i];
const statusData: InsertFormStatus = {
formId,
name: status.name,
color: status.color || '#e5e7eb',
position: i,
isInitial: status.isInitial || false,
isFinal: status.isFinal || false,
entryConditions: status.entryConditions ?? null,
};
if (status.id && status.id > 0) {
const [updated] = await tx
.update(formStatuses)
.set(statusData)
.where(and(
eq(formStatuses.id, status.id),
eq(formStatuses.formId, formId),
exists(tx.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
if (updated) resultStatuses.push(updated);
} else {
const [created] = await tx
.insert(formStatuses)
.values(statusData)
.returning();
resultStatuses.push(created);
}
}
return resultStatuses;
});
}
// Status Transitions
async getStatusTransitions(formId: number, organizationId: number): Promise<StatusTransition[]> {
return await db
.select()
.from(statusTransitions)
.innerJoin(forms, eq(statusTransitions.formId, forms.id))
.where(and(eq(statusTransitions.formId, formId), eq(forms.organizationId, organizationId)))
.orderBy(statusTransitions.id)
.then(results => results.map(r => r.status_transitions));
}
async createStatusTransition(insertTransition: InsertStatusTransition): Promise<StatusTransition> {
const [transition] = await db
.insert(statusTransitions)
.values(insertTransition)
.returning();
return transition;
}
async updateStatusTransition(id: number, formId: number, organizationId: number, updates: Partial<StatusTransition>): Promise<StatusTransition> {
const [transition] = await db
.update(statusTransitions)
.set(updates)
.where(and(
eq(statusTransitions.id, id),
eq(statusTransitions.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
))
.returning();
return transition;
}
async deleteStatusTransition(id: number, formId: number, organizationId: number): Promise<void> {
await db
.delete(statusTransitions)
.where(and(
eq(statusTransitions.id, id),
eq(statusTransitions.formId, formId),
exists(db.select().from(forms).where(and(eq(forms.id, formId), eq(forms.organizationId, organizationId))))
));
}
// Offline cache configuration
async getFormsWithOfflineCache(organizationId: number): Promise<Form[]> {
return await db
.select()
.from(forms)
.where(and(
eq(forms.organizationId, organizationId),
sql`${forms.offlineCache} IS NOT NULL`,
sql`(${forms.offlineCache}->>'enabled')::boolean = true`
))
.orderBy(desc(forms.createdAt));
}
async getOfflineCacheConfig(id: number, organizationId: number): Promise<Form['offlineCache']> {
const [form] = await db
.select({ offlineCache: forms.offlineCache })
.from(forms)
.where(and(eq(forms.id, id), eq(forms.organizationId, organizationId)));
return form?.offlineCache ?? null;
}
async updateFormOfflineCache(id: number, organizationId: number, config: Form['offlineCache']): Promise<Form> {
const [form] = await db
.update(forms)
.set({ offlineCache: config, updatedAt: new Date() })
.where(and(eq(forms.id, id), eq(forms.organizationId, organizationId)))
.returning();
if (!form) throw new Error("Форма не найдена");
return form;
}
}