Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ dist/
.local/
*.local.*
.vscode/
.idea/
# AI context files (auto-generated)
COPILOT.md
.DS_Store
GEMINI.md
CVE/*
.todo/*
.todo/*
.ai/
12 changes: 12 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 42 additions & 127 deletions routes/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const path = require('path');
const crypto = require('crypto');
const {
validateApiUrl,
validateCustomFieldValue,
shouldQueueForOcrOnAiError,
classifyOcrQueueReasonFromAiError,
} = require('../services/serviceUtils');
Expand All @@ -25,6 +24,11 @@ const QRCode = require('qrcode');
const { isAuthenticated } = require('./auth.js');
const customService = require('../services/customService.js');
const mistralOcrService = require('../services/mistralOcrService');
const {
updateCustomFieldsData,
updateDocumentTypeData,
updateCorrespondentData,
} = require('../services/dataProcessingUtils');
const quickstartService = require('../services/quickstartService');
const reconciliationService = require('../services/reconciliationService');
const {
Expand Down Expand Up @@ -3244,116 +3248,9 @@ async function buildUpdateData(analysis, doc) {
// Add created date regardless of settings as it's a core field
updateData.created = analysis.document.document_date || doc.created;

// Only process document type if document type classification is activated
if (
config.limitFunctions?.activateDocumentType !== 'no' &&
analysis.document.document_type
) {
try {
const documentType = await paperlessService.getOrCreateDocumentType(
analysis.document.document_type,
options
);
if (documentType) {
updateData.document_type = documentType.id;
}
} catch (error) {
console.error(`[ERROR] Error processing document type:`, error);
}
}

// Only process custom fields if custom fields detection is activated
if (
config.limitFunctions?.activateCustomFields !== 'no' &&
analysis.document.custom_fields
) {
const customFields = analysis.document.custom_fields;
const processedFields = [];
const customFieldsForHistory = [];

// Get existing custom fields
const existingFields = await paperlessService.getExistingCustomFields(
doc.id
);
console.log(`[DEBUG] Found existing fields:`, existingFields);

// Keep track of which fields we've processed to avoid duplicates
const processedFieldIds = new Set();

// First, add any new/updated fields
for (const customField of Object.values(customFields)) {
if (!customField || typeof customField !== 'object') {
console.log('[DEBUG] Skipping null/invalid custom field entry');
continue;
}

if (
!customField.field_name ||
customField.value === null ||
customField.value === undefined ||
String(customField.value).trim() === ''
) {
console.log(`[DEBUG] Skipping empty/invalid custom field`);
continue;
}

const fieldDetails = await paperlessService.findExistingCustomField(
customField.field_name
);
if (fieldDetails?.id) {
const validation = validateCustomFieldValue(
customField.field_name,
customField.value,
fieldDetails.data_type
);
if (validation.skip) {
if (validation.warn) console.warn(validation.warn);
continue;
}
processedFields.push({
field: fieldDetails.id,
value: validation.value,
});
customFieldsForHistory.push({
field_name: customField.field_name,
value: validation.value,
});
processedFieldIds.add(fieldDetails.id);
}
}

// Then add any existing fields that weren't updated
for (const existingField of existingFields) {
if (!processedFieldIds.has(existingField.field)) {
processedFields.push(existingField);
}
}

if (processedFields.length > 0) {
updateData.custom_fields = processedFields;
}
if (customFieldsForHistory.length > 0) {
updateData._customFieldsForHistory = customFieldsForHistory;
}
}

// Only process correspondent if correspondent detection is activated
if (
config.limitFunctions?.activateCorrespondents !== 'no' &&
analysis.document.correspondent
) {
try {
const correspondent = await paperlessService.getOrCreateCorrespondent(
analysis.document.correspondent,
options
);
if (correspondent) {
updateData.correspondent = correspondent.id;
}
} catch (error) {
console.error(`[ERROR] Error processing correspondent:`, error);
}
}
await updateDocumentTypeData(analysis, updateData, config, paperlessService, options);
await updateCustomFieldsData(analysis, doc, updateData, config, paperlessService);
await updateCorrespondentData(analysis, updateData, config, paperlessService, options);

// Always include language if provided as it's a core field
if (analysis.document.language) {
Expand All @@ -3378,16 +3275,30 @@ async function saveDocumentChanges(docId, updateData, analysis, originalData) {
const origDocType = originalData.document_type ?? null;
const origLanguage = originalData.language ?? null;

await documentModel.saveOriginalData(
docId,
originalTags,
originalCorrespondent,
originalTitle,
origDocType,
origLanguage
);

const updatedDocument = await paperlessService.updateDocument(docId, updateData);
if (!updatedDocument) {
throw new Error(`Paperless update failed for document ${docId}`);
}

const currentCorrespondentId = updatedDocument.correspondent ?? updateData.correspondent;
const currentCorrespondent = currentCorrespondentId
? await paperlessService.getCorrespondentNameById(currentCorrespondentId)
: null;
const currentDocumentTypeId = updatedDocument.document_type ?? updateData.document_type;
const currentDocumentType = currentDocumentTypeId
? await paperlessService.getDocumentTypeNameById(currentDocumentTypeId)
: null;

await Promise.all([
documentModel.saveOriginalData(
docId,
originalTags,
originalCorrespondent,
originalTitle,
origDocType,
origLanguage
),
paperlessService.updateDocument(docId, updateData),
documentModel.addProcessedDocument(docId, updateData.title),
documentModel.addOpenAIMetrics(
docId,
Expand All @@ -3399,9 +3310,9 @@ async function saveDocumentChanges(docId, updateData, analysis, originalData) {
docId,
updateData.tags,
updateData.title,
analysis.document.correspondent,
currentCorrespondent?.name ?? null,
historyCustomFields,
historyDocTypeName,
currentDocumentType?.name ?? null,
historyLanguage
),
]);
Expand Down Expand Up @@ -8520,10 +8431,6 @@ router.post('/settings', express.json(), async (req, res) => {
...updatedConfig,
};

// The route has already validated the submitted values (and Paperless
// reachability when its settings changed). Skip saveConfig's built-in
// validateConfig so saving does not live-test Paperless/AI on every save.
await setupService.saveConfig(mergedConfig, { skipValidation: true });
try {
for (const field of processedCustomFields) {
await paperlessService.createCustomFieldSafely(
Expand All @@ -8533,9 +8440,17 @@ router.post('/settings', express.json(), async (req, res) => {
);
}
} catch (error) {
console.log('[ERROR] Error creating custom fields:', error);
if (error.code === 'CUSTOM_FIELD_TYPE_MISMATCH') {
return res.status(400).json({ error: error.message });
}
throw error;
}

// The route has already validated the submitted values (and Paperless
// reachability when its settings changed). Skip saveConfig's built-in
// validateConfig so saving does not live-test Paperless/AI on every save.
await setupService.saveConfig(mergedConfig, { skipValidation: true });

res.json({
success: true,
message: 'Configuration saved successfully.',
Expand Down
104 changes: 19 additions & 85 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const jwt = require('jsonwebtoken');
const Logger = require('./services/loggerService');
const { max } = require('date-fns');
const {
validateCustomFieldValue,
updateCustomFieldsData,
updateDocumentTypeData,
updateCorrespondentData
} = require('./services/dataProcessingUtils');
const {
shouldQueueForOcrOnAiError,
classifyOcrQueueReasonFromAiError,
isTimeoutError,
Expand Down Expand Up @@ -684,88 +688,9 @@ async function buildUpdateData(analysis, doc) {
// Add created date regardless of settings as it's a core field
updateData.created = analysis.document.document_date || doc.created;

// Only process document type if document type classification is activated
if (config.limitFunctions?.activateDocumentType !== 'no' && analysis.document.document_type) {
try {
const documentType = await paperlessService.getOrCreateDocumentType(analysis.document.document_type, options);
if (documentType) {
updateData.document_type = documentType.id;
}
} catch (error) {
console.error(`[ERROR] Error processing document type: ${error.message}`);
console.debug(error);
}
}

// Only process custom fields if custom fields detection is activated
if (config.limitFunctions?.activateCustomFields !== 'no' && analysis.document.custom_fields) {
const customFields = analysis.document.custom_fields;
const processedFields = [];
const customFieldsForHistory = [];

// Get existing custom fields
const existingFields = await paperlessService.getExistingCustomFields(doc.id);
console.debug('Found existing fields:', existingFields);

// Keep track of which fields we've processed to avoid duplicates
const processedFieldIds = new Set();

// First, add any new/updated fields
for (const key in customFields) {
const customField = customFields[key];

if (!customField.field_name || (customField.value === null || customField.value === undefined || String(customField.value).trim() === '')) {
console.debug('Skipping empty or invalid custom field');
continue;
}

const fieldDetails = await paperlessService.findExistingCustomField(customField.field_name);
if (fieldDetails?.id) {
const validation = validateCustomFieldValue(customField.field_name, customField.value, fieldDetails.data_type);
if (validation.skip) {
if (validation.warn) console.warn(validation.warn);
continue;
}
processedFields.push({
field: fieldDetails.id,
value: validation.value
});
// Capture name + validated value for history at the point where we have both
customFieldsForHistory.push({
field_name: customField.field_name,
value: validation.value
});
processedFieldIds.add(fieldDetails.id);
}
}

// Then add any existing fields that weren't updated
for (const existingField of existingFields) {
if (!processedFieldIds.has(existingField.field)) {
processedFields.push(existingField);
}
}

if (processedFields.length > 0) {
updateData.custom_fields = processedFields;
}
if (customFieldsForHistory.length > 0) {
updateData._customFieldsForHistory = customFieldsForHistory;
}
}

// Only process correspondent if correspondent detection is activated
if (config.limitFunctions?.activateCorrespondents !== 'no' && analysis.document.correspondent) {
try {
const correspondent = await paperlessService.getOrCreateCorrespondent(analysis.document.correspondent, options);
if (correspondent) {
updateData.correspondent = correspondent.id;
}
} catch (error) {
console.error(`[ERROR] Error processing correspondent: ${error.message}`);
console.debug(error);
}
}
await updateDocumentTypeData(analysis, updateData, config, paperlessService, options);
await updateCustomFieldsData(analysis, doc, updateData, config, paperlessService);
await updateCorrespondentData(analysis, updateData, config, paperlessService, options);

// Always include language if provided as it's a core field
if (analysis.document.language) {
Expand Down Expand Up @@ -794,15 +719,24 @@ async function saveDocumentChanges(docId, updateData, analysis, originalData) {
throw new Error(`Paperless update failed for document ${docId}`);
}

const currentCorrespondentId = updatedDocument.correspondent ?? updateData.correspondent;
const currentCorrespondent = currentCorrespondentId
? await paperlessService.getCorrespondentNameById(currentCorrespondentId)
: null;
const currentDocumentTypeId = updatedDocument.document_type ?? updateData.document_type;
const currentDocumentType = currentDocumentTypeId
? await paperlessService.getDocumentTypeNameById(currentDocumentTypeId)
: null;

const persistenceTasks = [
documentModel.addProcessedDocument(docId, updateData.title),
documentModel.addToHistory(
docId,
updateData.tags,
updateData.title,
analysis.document.correspondent,
currentCorrespondent?.name ?? null,
historyCustomFields,
historyDocTypeName,
currentDocumentType?.name ?? null,
historyLanguage
)
];
Expand Down
Loading