From 9d4ec4ff964f9a2fb4c95e5c4f23161347a0c96d Mon Sep 17 00:00:00 2001 From: David Rotermund Date: Sun, 25 Oct 2026 18:04:09 +0000 Subject: [PATCH] fix: cloning projects failed with zotero- #253 --- .../zotero/app/src/ZoteroLinkedFileAgent.mjs | 21 ++++- ...000_ce_sp_zotero_linked_file_data_keys.mjs | 77 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tools/migrations/20261001000000_ce_sp_zotero_linked_file_data_keys.mjs diff --git a/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs b/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs index 6c595a669e..f315329bc2 100644 --- a/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs +++ b/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs @@ -68,6 +68,7 @@ async function refreshLinkedFile( parentFolderId, userId ) { + linkedFileData = _normalizeLegacyData(linkedFileData) logger.debug( { projectId, userId, linkedFileData }, 'refreshing Zotero linked file' @@ -108,7 +109,7 @@ async function _getBibtex(linkedFileData) { return await ZoteroApiClient.getLibraryBibtex( userId, linkedFileData.group_id, // == null for main library - linkedFileData.format || 'bibtex' + linkedFileData.format ) } catch (err) { @@ -129,6 +130,24 @@ async function _getBibtex(linkedFileData) { } } +// Files imported before "Zotero: fix for 6.3.0" use the old key names. +function _normalizeLegacyData(data) { + const { + zoteroGroupId, + importedByUserId, + importedByName, + bibFormat, + ...rest + } = data + return { + ...rest, + group_id: rest.group_id ?? zoteroGroupId ?? null, + importer_id: rest.importer_id ?? importedByUserId, + importer_name: rest.importer_name ?? importedByName, + format: rest.format ?? bibFormat, + } +} + function _sanitizeData(data) { return { provider: 'zotero', diff --git a/tools/migrations/20261001000000_ce_sp_zotero_linked_file_data_keys.mjs b/tools/migrations/20261001000000_ce_sp_zotero_linked_file_data_keys.mjs new file mode 100644 index 0000000000..8dfef31a71 --- /dev/null +++ b/tools/migrations/20261001000000_ce_sp_zotero_linked_file_data_keys.mjs @@ -0,0 +1,77 @@ +import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js' +import { promiseMapWithLimit } from '@overleaf/promise-utils' +import { db } from './lib/mongodb.mjs' + +const tags = ['server-ce', 'server-pro'] + +const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY, 10) || 10 + +// The Zotero module used to store linkedFileData with its own key names. +// "Zotero: fix for 6.3.0" switched to the names accepted by the +// overleaf-editor-core schema, but files imported before that still carry the +// old keys, which document-updater rejects (e.g. when cloning the project). +const LEGACY_KEYS = [ + 'zoteroGroupId', + 'importedByUserId', + 'importedByName', + 'bibFormat', +] + +function hasLegacyKeys(data) { + return ( + data?.provider === 'zotero' && LEGACY_KEYS.some(key => key in data) + ) +} + +function convert(data) { + const importerId = data.importer_id ?? data.importedByUserId + const format = data.format ?? data.bibFormat + return { + provider: 'zotero', + group_id: data.group_id ?? data.zoteroGroupId ?? null, + ...(data.importedAt && { importedAt: data.importedAt }), + ...(importerId && { importer_id: importerId.toString() }), + importer_name: data.importer_name ?? data.importedByName ?? 'Unknown', + format: format === 'biblatex' ? 'biblatex' : 'bibtex', + } +} + +function collectUpdates(folder, mongoPath, updates) { + if (!folder) return + ;(folder.fileRefs || []).forEach((file, i) => { + if (hasLegacyKeys(file?.linkedFileData)) { + updates[`${mongoPath}.fileRefs.${i}.linkedFileData`] = convert( + file.linkedFileData + ) + } + }) + ;(folder.folders || []).forEach((subfolder, i) => { + collectUpdates(subfolder, `${mongoPath}.folders.${i}`, updates) + }) +} + +const migrate = async () => { + await batchedUpdate( + db.projects, + {}, + async nextBatch => { + await promiseMapWithLimit(WRITE_CONCURRENCY, nextBatch, async project => { + const updates = {} + ;(project.rootFolder || []).forEach((folder, i) => { + collectUpdates(folder, `rootFolder.${i}`, updates) + }) + if (Object.keys(updates).length === 0) return + await db.projects.updateOne({ _id: project._id }, { $set: updates }) + }) + }, + { _id: 1, rootFolder: 1 } + ) +} + +const rollback = async () => {} + +export default { + tags, + migrate, + rollback, +}