|
@@ -0,0 +1,402 @@
|
|
|
+import {
|
|
|
+ CustomFieldWithValidators,
|
|
|
+ CustomFieldWithValidatorsAndValue,
|
|
|
+ FieldTypeCode,
|
|
|
+ InputFieldValue,
|
|
|
+ SavedFieldValue,
|
|
|
+ Validator,
|
|
|
+} from "#api/v_0.1.0/types/custom-fields-types.js";
|
|
|
+import { z } from "zod";
|
|
|
+import { getValidationFunc } from "./validation-functions.js";
|
|
|
+import { ApiError } from "#exceptions/api-error.js";
|
|
|
+import { filesUtils } from "#utils/files-utils.js";
|
|
|
+import { DatabaseTransactionConnection, sql } from "slonik";
|
|
|
+import { logger } from "#plugins/logger.js";
|
|
|
+
|
|
|
+type CustomFieldInput = {
|
|
|
+ value: z.infer<typeof InputFieldValue>;
|
|
|
+} & (
|
|
|
+ | { arffId: string; peFfId?: never; userEfId?: never }
|
|
|
+ | { peFfId: string; arffId?: never; userEfId?: never }
|
|
|
+ | { userEfId: string; arffId?: never; peFfId?: never }
|
|
|
+);
|
|
|
+
|
|
|
+type MulterFiles =
|
|
|
+ | {
|
|
|
+ [fieldname: string]: Express.Multer.File[];
|
|
|
+ }
|
|
|
+ | Express.Multer.File[]
|
|
|
+ | undefined;
|
|
|
+
|
|
|
+const fileTypeCodes: z.infer<typeof FieldTypeCode>[] = ["audio"];
|
|
|
+
|
|
|
+class CCustomFieldsValidateService {
|
|
|
+ async validateFields(
|
|
|
+ fields: {
|
|
|
+ value: unknown;
|
|
|
+ fieldDefinitionId: string;
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ }[],
|
|
|
+ refFields: z.infer<typeof CustomFieldWithValidators>[],
|
|
|
+ ) {
|
|
|
+ let isValid = true;
|
|
|
+ const messages: string[] = [];
|
|
|
+
|
|
|
+ for (const field of fields) {
|
|
|
+ const refField = refFields.find(
|
|
|
+ (f) => f.fieldDefinitionId === field.fieldDefinitionId,
|
|
|
+ );
|
|
|
+ if (!refField) {
|
|
|
+ isValid = false;
|
|
|
+ messages.push(
|
|
|
+ `Поле fieldDefinitionId:"${field.fieldDefinitionId}" не заполнено`,
|
|
|
+ );
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const validator of refField.validators) {
|
|
|
+ const result = await this.validateField(field.value, validator);
|
|
|
+ if (!result.isValid) {
|
|
|
+ isValid = false;
|
|
|
+ messages.push(...result.messages);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ isValid,
|
|
|
+ messages,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async validateField(value: unknown, validator: z.infer<typeof Validator>) {
|
|
|
+ const validateFunc = getValidationFunc(validator);
|
|
|
+ if (!validateFunc) {
|
|
|
+ return {
|
|
|
+ isValid: true,
|
|
|
+ messages: [],
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const result = await validateFunc(value);
|
|
|
+ return {
|
|
|
+ isValid: result === true,
|
|
|
+ messages: result === true ? [] : [result],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async createFilesMap(files: MulterFiles) {
|
|
|
+ return new Map(
|
|
|
+ (() => {
|
|
|
+ if (!files) return [];
|
|
|
+ if (Array.isArray(files)) return files.map((f) => [f.fieldname, f]);
|
|
|
+ return Object.entries(files).flatMap(([fieldname, files]) =>
|
|
|
+ files.map((file) => [fieldname, file]),
|
|
|
+ );
|
|
|
+ })(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ //
|
|
|
+ //
|
|
|
+ //
|
|
|
+
|
|
|
+ async processAndValidateFields({
|
|
|
+ inputFields,
|
|
|
+ referenceFields,
|
|
|
+ files,
|
|
|
+ idKey,
|
|
|
+ addOldValue,
|
|
|
+ }: {
|
|
|
+ inputFields: CustomFieldInput[];
|
|
|
+ referenceFields: (z.infer<typeof CustomFieldWithValidators> & {
|
|
|
+ idKey: string;
|
|
|
+ })[];
|
|
|
+ files: MulterFiles;
|
|
|
+ idKey: "arffId" | "peFfId" | "userEfId"; // Ключ для идентификации поля в объектах и файлах
|
|
|
+ addOldValue: false;
|
|
|
+ }): Promise<{
|
|
|
+ isValid: boolean;
|
|
|
+ messages: string[];
|
|
|
+ checkedfields: (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ })[];
|
|
|
+ }>;
|
|
|
+ async processAndValidateFields({
|
|
|
+ inputFields,
|
|
|
+ referenceFields,
|
|
|
+ files,
|
|
|
+ idKey,
|
|
|
+ addOldValue,
|
|
|
+ }: {
|
|
|
+ inputFields: CustomFieldInput[];
|
|
|
+ referenceFields: (z.infer<typeof CustomFieldWithValidatorsAndValue> & {
|
|
|
+ idKey: string;
|
|
|
+ })[];
|
|
|
+ files: MulterFiles;
|
|
|
+ idKey: "arffId" | "peFfId" | "userEfId"; // Ключ для идентификации поля в объектах и файлах
|
|
|
+ addOldValue: true;
|
|
|
+ }): Promise<{
|
|
|
+ isValid: boolean;
|
|
|
+ messages: string[];
|
|
|
+ checkedfields: (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ oldValue: z.infer<typeof InputFieldValue>;
|
|
|
+ })[];
|
|
|
+ }>;
|
|
|
+ async processAndValidateFields({
|
|
|
+ inputFields,
|
|
|
+ referenceFields,
|
|
|
+ files,
|
|
|
+ idKey,
|
|
|
+ addOldValue,
|
|
|
+ }: {
|
|
|
+ inputFields: CustomFieldInput[];
|
|
|
+ referenceFields: (
|
|
|
+ | (z.infer<typeof CustomFieldWithValidators> & {
|
|
|
+ idKey: string;
|
|
|
+ })
|
|
|
+ | (z.infer<typeof CustomFieldWithValidatorsAndValue> & {
|
|
|
+ idKey: string;
|
|
|
+ })
|
|
|
+ )[];
|
|
|
+ files: MulterFiles;
|
|
|
+ idKey: "arffId" | "peFfId" | "userEfId"; // Ключ для идентификации поля в объектах и файлах
|
|
|
+ addOldValue: boolean;
|
|
|
+ }): Promise<{
|
|
|
+ isValid: boolean;
|
|
|
+ messages: string[];
|
|
|
+ checkedfields: (
|
|
|
+ | (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ })
|
|
|
+ | (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ oldValue: z.infer<typeof InputFieldValue>;
|
|
|
+ })
|
|
|
+ )[];
|
|
|
+ }> {
|
|
|
+ // Проверка, что все нужные поля есть
|
|
|
+ const checkedfields: (
|
|
|
+ | (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ })
|
|
|
+ | (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ oldValue: z.infer<typeof InputFieldValue>;
|
|
|
+ })
|
|
|
+ )[] = [];
|
|
|
+
|
|
|
+ for (const refField of referenceFields) {
|
|
|
+ const field = inputFields.find((ff) => ff[idKey] === refField[idKey]);
|
|
|
+ if (!field) {
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "fieldNotFound",
|
|
|
+ `Поле ${refField.fieldDefinitionId} (id: ${refField[idKey]}) не найдено`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ checkedfields.push({
|
|
|
+ ...field,
|
|
|
+ fieldType: refField.fieldTypeCode,
|
|
|
+ oldValue:
|
|
|
+ addOldValue && "value" in refField ? refField.value : undefined,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Подготовка fieldsToCheck
|
|
|
+ const filesMap = await this.createFilesMap(files);
|
|
|
+
|
|
|
+ const fieldsToCheck = checkedfields.map((f) => {
|
|
|
+ const refField = referenceFields.find((rf) => rf[idKey] === f[idKey]);
|
|
|
+ if (!refField) {
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "fieldNotFound",
|
|
|
+ `Consistency error: refField not found for ${idKey} ${f[idKey]}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ value: fileTypeCodes.includes(f.fieldType)
|
|
|
+ ? // если файловый тип
|
|
|
+ filesMap.get(f[idKey]!)
|
|
|
+ ? // если есть такой файл
|
|
|
+ filesUtils.convertMulterFileToStandardFile(
|
|
|
+ filesMap.get(f[idKey]!)!,
|
|
|
+ )
|
|
|
+ : // если нет файла
|
|
|
+ null
|
|
|
+ : // если не файл
|
|
|
+ f.value,
|
|
|
+ fieldDefinitionId: refField.fieldDefinitionId,
|
|
|
+ fieldType: f.fieldType,
|
|
|
+ [idKey]: f[idKey],
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // Валидация
|
|
|
+ const validationResult = await this.validateFields(
|
|
|
+ fieldsToCheck.map((f) => ({ ...f, value: f.value })), // value уже обработано
|
|
|
+ referenceFields,
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ isValid: validationResult.isValid,
|
|
|
+ messages: validationResult.messages,
|
|
|
+ checkedfields,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async saveCustomFieldValuesInTransaction({
|
|
|
+ tr,
|
|
|
+ parentId,
|
|
|
+ action,
|
|
|
+ inputFields,
|
|
|
+ files,
|
|
|
+ isDeleteBefore,
|
|
|
+ }: {
|
|
|
+ tr: DatabaseTransactionConnection;
|
|
|
+ parentId: string;
|
|
|
+ action: "activityPeReg" | "peCreate" | "userProfile";
|
|
|
+ inputFields: (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ })[];
|
|
|
+ files: MulterFiles;
|
|
|
+ isDeleteBefore: false;
|
|
|
+ }): Promise<void>;
|
|
|
+ async saveCustomFieldValuesInTransaction({
|
|
|
+ tr,
|
|
|
+ parentId,
|
|
|
+ action,
|
|
|
+ inputFields,
|
|
|
+ files,
|
|
|
+ isDeleteBefore,
|
|
|
+ }: {
|
|
|
+ tr: DatabaseTransactionConnection;
|
|
|
+ parentId: string;
|
|
|
+ action: "activityPeReg" | "peCreate" | "userProfile";
|
|
|
+ inputFields: (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ oldValue: z.infer<typeof InputFieldValue>;
|
|
|
+ })[];
|
|
|
+ files: MulterFiles;
|
|
|
+ isDeleteBefore: true;
|
|
|
+ }): Promise<void>;
|
|
|
+ async saveCustomFieldValuesInTransaction({
|
|
|
+ tr,
|
|
|
+ parentId,
|
|
|
+ action,
|
|
|
+ inputFields,
|
|
|
+ files,
|
|
|
+ isDeleteBefore,
|
|
|
+ }: {
|
|
|
+ tr: DatabaseTransactionConnection;
|
|
|
+ parentId: string;
|
|
|
+ action: "activityPeReg" | "peCreate" | "userProfile";
|
|
|
+ inputFields: (CustomFieldInput & {
|
|
|
+ fieldType: z.infer<typeof FieldTypeCode>;
|
|
|
+ oldValue?: z.infer<typeof InputFieldValue>;
|
|
|
+ })[];
|
|
|
+ files: MulterFiles;
|
|
|
+ isDeleteBefore: boolean;
|
|
|
+ }): Promise<void> {
|
|
|
+ // перменные для разных типов
|
|
|
+ let parentTableIdColumn: string; // "activity_reg_id" или "pe_id"
|
|
|
+ let valuesTable: string; // "act.ar_field_values" или "act.pe_field_values"
|
|
|
+ let idKeyForValuesTable: string; // "arff_id" или "pe_ff_id"
|
|
|
+ let idKeyForFields: "arffId" | "peFfId" | "userEfId";
|
|
|
+
|
|
|
+ if (action === "activityPeReg") {
|
|
|
+ parentTableIdColumn = "activity_reg_id";
|
|
|
+ valuesTable = "act.ar_field_values";
|
|
|
+ idKeyForValuesTable = "arff_id";
|
|
|
+ idKeyForFields = "arffId";
|
|
|
+ } else if (action === "peCreate") {
|
|
|
+ parentTableIdColumn = "pe_id";
|
|
|
+ valuesTable = "act.pe_field_values";
|
|
|
+ idKeyForValuesTable = "pe_ff_id";
|
|
|
+ idKeyForFields = "peFfId";
|
|
|
+ } else if (action === "userProfile") {
|
|
|
+ parentTableIdColumn = "user_id";
|
|
|
+ valuesTable = "ev.user_event_field_values";
|
|
|
+ idKeyForValuesTable = "user_ef_id";
|
|
|
+ idKeyForFields = "userEfId";
|
|
|
+ } else {
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "actionNotFound",
|
|
|
+ `Consistency error: action ${action} not found`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ //
|
|
|
+
|
|
|
+ const filesMap = await this.createFilesMap(files);
|
|
|
+
|
|
|
+ for (const field of inputFields) {
|
|
|
+ const keyId = field[idKeyForFields];
|
|
|
+ if (!keyId)
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "keyIdNotFound",
|
|
|
+ `Consistency error: keyId not found for ${idKeyForFields} ${field[idKeyForFields]}`,
|
|
|
+ );
|
|
|
+ let valueToSave: z.infer<typeof SavedFieldValue>;
|
|
|
+
|
|
|
+ // удаляем старые значения
|
|
|
+ if (isDeleteBefore) {
|
|
|
+ await tr.query(sql.unsafe`
|
|
|
+ delete from ${sql.identifier([...valuesTable.split(".")])}
|
|
|
+ where
|
|
|
+ ${sql.identifier([parentTableIdColumn])} = ${parentId}
|
|
|
+ and ${sql.identifier([idKeyForValuesTable])} = ${keyId};
|
|
|
+ `);
|
|
|
+
|
|
|
+ logger.info(`field.oldValue`, field.oldValue);
|
|
|
+ if (
|
|
|
+ field.oldValue &&
|
|
|
+ typeof field.oldValue === "object" &&
|
|
|
+ field.oldValue.fileName
|
|
|
+ ) {
|
|
|
+ logger.info(`Deleting old file ${field.oldValue.fileName}`);
|
|
|
+ filesUtils.deleteFile(field.oldValue.fileName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const file = filesMap.get(keyId);
|
|
|
+ if (fileTypeCodes.includes(field.fieldType) && field.value !== null) {
|
|
|
+ if (!file) {
|
|
|
+ // Здесь предполагаем, что если fieldType=файловый тип, то файл должен быть.
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "fileNotFound",
|
|
|
+ `Файл поля ${idKeyForFields}:${keyId} не найден на этапе сохранения`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const savedFile = await filesUtils.saveFile(file);
|
|
|
+ valueToSave = {
|
|
|
+ originalName: savedFile.originalname,
|
|
|
+ fileName: savedFile.filename,
|
|
|
+ type: savedFile.type,
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ if (
|
|
|
+ typeof field.value !== "string" &&
|
|
|
+ typeof field.value !== "number" &&
|
|
|
+ typeof field.value !== "boolean" &&
|
|
|
+ field.value !== null
|
|
|
+ ) {
|
|
|
+ throw ApiError.BadRequest(
|
|
|
+ "valueNotString",
|
|
|
+ `Ошибка типа данных кастомного поля. Value: ${field.value}. Type: ${typeof field.value}. Field: ${field.arffId || field.peFfId || field.userEfId}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ valueToSave = field.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ await tr.query(sql.unsafe`
|
|
|
+ insert into ${sql.identifier([...valuesTable.split(".")])}
|
|
|
+ (${sql.identifier([parentTableIdColumn])}, ${sql.identifier([idKeyForValuesTable])}, value)
|
|
|
+ values
|
|
|
+ (${parentId}, ${keyId}, ${valueToSave ? sql.jsonb(valueToSave) : null})
|
|
|
+ `);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const cCustomFieldsValidateService = new CCustomFieldsValidateService();
|