mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Merge e8a9ce351b into ff93139cce
This commit is contained in:
commit
ddcc5e075f
2 changed files with 852 additions and 0 deletions
|
|
@ -289,6 +289,276 @@ Zotero.Server.Connector.Detect.prototype = {
|
|||
},
|
||||
}
|
||||
|
||||
Zotero.Server.Connector.FindExistingItems = function () {};
|
||||
Zotero.Server.Endpoints["/connector/findExistingItems"] = Zotero.Server.Connector.FindExistingItems;
|
||||
Zotero.Server.Connector.FindExistingItems.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/json"],
|
||||
permitBookmarklet: true,
|
||||
|
||||
init: async function (requestData) {
|
||||
let data = requestData.data || {};
|
||||
let identifiers = Zotero.Server.Connector._getItemIdentifiers(data);
|
||||
if (!identifiers.doi.length && !identifiers.url.length) {
|
||||
return [400, "application/json", JSON.stringify({ error: "IDENTIFIERS_NOT_PROVIDED" })];
|
||||
}
|
||||
|
||||
let target;
|
||||
if (data.target) {
|
||||
try {
|
||||
target = Zotero.Server.Connector.resolveTarget(data.target);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(`Could not resolve duplicate lookup target '${data.target}': ${e.message}`);
|
||||
}
|
||||
}
|
||||
let { library } = target && target.library
|
||||
? target
|
||||
: Zotero.Server.Connector.getSaveTarget();
|
||||
let matches = await Zotero.Server.Connector.findExistingItemsByIdentifiers(
|
||||
identifiers,
|
||||
library.libraryID
|
||||
);
|
||||
return [200, "application/json", JSON.stringify({ matches })];
|
||||
}
|
||||
};
|
||||
|
||||
Zotero.Server.Connector._getItemIdentifiers = function (data) {
|
||||
let identifiers = {
|
||||
doi: new Set(),
|
||||
url: new Set()
|
||||
};
|
||||
let itemIdentifiers = [];
|
||||
let proxy = data.proxy && new Zotero.Proxy(data.proxy);
|
||||
|
||||
let addDOI = (doi) => {
|
||||
doi = doi && Zotero.Utilities.cleanDOI(doi);
|
||||
if (doi) {
|
||||
doi = doi.toLowerCase();
|
||||
identifiers.doi.add(doi);
|
||||
return doi;
|
||||
}
|
||||
};
|
||||
let addURL = (url) => {
|
||||
if (proxy && url) {
|
||||
try {
|
||||
// Only deproxify when the proxy regexp actually matches.
|
||||
// toProper() normalizes non-matching URLs through new URL().href,
|
||||
// which can break exact-match lookups against values stored from
|
||||
// the translator (e.g. https://example.com vs https://example.com/).
|
||||
let normalized = new URL(url).href;
|
||||
if (proxy.regexp.test(normalized)) {
|
||||
url = proxy.toProper(normalized);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(`Could not deproxify item URL for duplicate lookup: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (url && typeof url == 'string') {
|
||||
identifiers.url.add(url);
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
if (data.identifiers) {
|
||||
for (let doi of data.identifiers.doi || []) {
|
||||
addDOI(doi);
|
||||
}
|
||||
for (let url of data.identifiers.url || []) {
|
||||
addURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
for (let item of data.items || []) {
|
||||
let itemIdentifier = {
|
||||
doi: new Set(),
|
||||
url: new Set()
|
||||
};
|
||||
let addItemDOI = (doi) => {
|
||||
doi = addDOI(doi);
|
||||
if (doi) {
|
||||
itemIdentifier.doi.add(doi);
|
||||
}
|
||||
};
|
||||
let addItemURL = (url) => {
|
||||
url = addURL(url);
|
||||
if (url) {
|
||||
itemIdentifier.url.add(url);
|
||||
}
|
||||
};
|
||||
addItemDOI(item.DOI);
|
||||
if (item.extra) {
|
||||
let { fields } = Zotero.Utilities.Internal.extractExtraFields(item.extra);
|
||||
addItemDOI(fields.get('DOI'));
|
||||
}
|
||||
addItemURL(item.url);
|
||||
itemIdentifiers.push({
|
||||
doi: Array.from(itemIdentifier.doi),
|
||||
url: Array.from(itemIdentifier.url)
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
doi: Array.from(identifiers.doi),
|
||||
url: Array.from(identifiers.url),
|
||||
itemIdentifiers
|
||||
};
|
||||
};
|
||||
|
||||
Zotero.Server.Connector.findExistingItemsByIdentifiers = async function (identifiers, libraryID) {
|
||||
let itemIDs = new Set();
|
||||
let doiSet = new Set(identifiers.doi);
|
||||
let urlSet = new Set(identifiers.url);
|
||||
let requestItemIdentifiers = identifiers.itemIdentifiers || [];
|
||||
let matches = [];
|
||||
let getMatchedItemIndexes = function (matchedDOIs, matchedURL) {
|
||||
if (!requestItemIdentifiers.length) {
|
||||
return [];
|
||||
}
|
||||
let indexes = [];
|
||||
for (let i = 0; i < requestItemIdentifiers.length; i++) {
|
||||
let itemIdentifiers = requestItemIdentifiers[i];
|
||||
if (matchedDOIs.some(doi => itemIdentifiers.doi.includes(doi))) {
|
||||
indexes.push(i);
|
||||
continue;
|
||||
}
|
||||
if (matchedURL && itemIdentifiers.url.includes(matchedURL)) {
|
||||
indexes.push(i);
|
||||
}
|
||||
}
|
||||
return indexes;
|
||||
};
|
||||
|
||||
if (doiSet.size) {
|
||||
let doiCandidateChunkSize = Math.min(100, Zotero.DB.MAX_BOUND_PARAMETERS - 2);
|
||||
let getDOICandidateRows = async function (fieldID) {
|
||||
let allRows = [];
|
||||
await Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
identifiers.doi,
|
||||
doiCandidateChunkSize,
|
||||
async function (chunk) {
|
||||
let rows = await Zotero.DB.queryAsync(
|
||||
"SELECT itemID, value FROM items JOIN itemData USING (itemID) "
|
||||
+ "JOIN itemDataValues USING (valueID) "
|
||||
+ "WHERE libraryID=? AND fieldID=? "
|
||||
+ "AND (" + chunk.map(() => "LOWER(value) LIKE ?").join(" OR ") + ") "
|
||||
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)",
|
||||
[
|
||||
libraryID,
|
||||
fieldID,
|
||||
...chunk.map(doi => `%${doi}%`)
|
||||
]
|
||||
);
|
||||
allRows.push(...rows);
|
||||
}
|
||||
);
|
||||
return allRows;
|
||||
};
|
||||
|
||||
let doiRows = await getDOICandidateRows(Zotero.ItemFields.getID('DOI'));
|
||||
for (let row of doiRows) {
|
||||
let doi = Zotero.Utilities.cleanDOI(row.value);
|
||||
if (doi && doiSet.has(doi.toLowerCase())) {
|
||||
itemIDs.add(row.itemID);
|
||||
}
|
||||
}
|
||||
let extraRows = await getDOICandidateRows(Zotero.ItemFields.getID('extra'));
|
||||
for (let row of extraRows) {
|
||||
let { fields } = Zotero.Utilities.Internal.extractExtraFields(row.value);
|
||||
let extraDOI = fields.get('DOI');
|
||||
let doi = extraDOI && Zotero.Utilities.cleanDOI(extraDOI);
|
||||
if (doi && doiSet.has(doi.toLowerCase())) {
|
||||
itemIDs.add(row.itemID);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlSet.size) {
|
||||
await Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
identifiers.url,
|
||||
Zotero.DB.MAX_BOUND_PARAMETERS - 2,
|
||||
async function (chunk) {
|
||||
let rows = await Zotero.DB.queryAsync(
|
||||
"SELECT itemID, value FROM items JOIN itemData USING (itemID) "
|
||||
+ "JOIN itemDataValues USING (valueID) "
|
||||
+ "WHERE libraryID=? AND fieldID=? "
|
||||
+ "AND value IN (" + chunk.map(() => '?').join(',') + ") "
|
||||
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)",
|
||||
[
|
||||
libraryID,
|
||||
Zotero.ItemFields.getID('url'),
|
||||
...chunk
|
||||
]
|
||||
);
|
||||
for (let row of rows) {
|
||||
if (urlSet.has(row.value)) {
|
||||
itemIDs.add(row.itemID);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (let itemID of itemIDs) {
|
||||
let item = await Zotero.Items.getAsync(itemID);
|
||||
if (!item || !item.isTopLevelItem()) {
|
||||
continue;
|
||||
}
|
||||
await item.loadDataType('itemData');
|
||||
|
||||
let matchedFields = [];
|
||||
let matchedIdentifiers = {};
|
||||
let itemDOIs = [
|
||||
item.getField('DOI'),
|
||||
item.getExtraField('DOI')
|
||||
]
|
||||
.map(doi => Zotero.Utilities.cleanDOI(doi))
|
||||
.filter(doi => doi && doiSet.has(doi.toLowerCase()));
|
||||
let matchedDOIs = Array.from(new Set(itemDOIs.map(doi => doi.toLowerCase())));
|
||||
if (itemDOIs.length) {
|
||||
matchedFields.push('DOI');
|
||||
matchedIdentifiers.doi = itemDOIs[0];
|
||||
}
|
||||
let itemURL = item.getField('url');
|
||||
if (itemURL && urlSet.has(itemURL)) {
|
||||
matchedFields.push('url');
|
||||
matchedIdentifiers.url = itemURL;
|
||||
}
|
||||
if (!matchedFields.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let match = {
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
libraryID: item.libraryID,
|
||||
title: item.getDisplayTitle(),
|
||||
matchedFields,
|
||||
matchedIdentifiers
|
||||
};
|
||||
let matchedItemIndexes = getMatchedItemIndexes(matchedDOIs, matchedIdentifiers.url);
|
||||
if (matchedItemIndexes.length) {
|
||||
// Keep the singular field aligned with the identifiers older
|
||||
// Connector versions receive.
|
||||
let compatibilityDOIs = matchedIdentifiers.doi
|
||||
? [matchedIdentifiers.doi.toLowerCase()]
|
||||
: [];
|
||||
let matchedItemIndex = getMatchedItemIndexes(
|
||||
compatibilityDOIs,
|
||||
matchedIdentifiers.url
|
||||
)[0];
|
||||
if (matchedItemIndex !== undefined) {
|
||||
match.matchedItemIndex = matchedItemIndex;
|
||||
}
|
||||
match.matchedItemIndexes = matchedItemIndexes;
|
||||
}
|
||||
matches.push(match);
|
||||
}
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves items to DB
|
||||
*
|
||||
|
|
|
|||
|
|
@ -320,6 +320,588 @@ describe("Connector Server", function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe("/connector/findExistingItems", function () {
|
||||
it("should return matching top-level items for DOI and URL in the selected target library", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Article");
|
||||
item.setField("DOI", "10.1234/Example.1");
|
||||
item.setField("url", "https://example.com/article");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
doi: ["10.1234/example.1"],
|
||||
url: ["https://example.com/article"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].libraryID, Zotero.Libraries.userLibraryID);
|
||||
assert.sameMembers(data.matches[0].matchedFields, ["DOI", "url"]);
|
||||
});
|
||||
|
||||
it("should use the requested target library for duplicate lookup", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let userItem = new Zotero.Item("journalArticle");
|
||||
userItem.setField("title", "Existing User Article");
|
||||
userItem.setField("DOI", "10.1234/targeted");
|
||||
await userItem.saveTx();
|
||||
|
||||
let group = await createGroup({ editable: true });
|
||||
let groupItem = new Zotero.Item("journalArticle");
|
||||
groupItem.libraryID = group.libraryID;
|
||||
groupItem.setField("title", "Existing Group Article");
|
||||
groupItem.setField("DOI", "10.1234/targeted");
|
||||
await groupItem.saveTx();
|
||||
Zotero.Items.unload([userItem.id, groupItem.id]);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
target: group.treeViewID,
|
||||
identifiers: {
|
||||
doi: ["10.1234/targeted"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, groupItem.id);
|
||||
assert.equal(data.matches[0].libraryID, group.libraryID);
|
||||
});
|
||||
|
||||
it("should deproxify item URLs before duplicate lookup", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("webpage");
|
||||
item.setField("title", "Existing Proxied Page");
|
||||
item.setField("url", "https://duplicate.example.com/path");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
proxy: {
|
||||
scheme: "https://%h.proxy.example.com/%p"
|
||||
},
|
||||
items: [{
|
||||
itemType: "webpage",
|
||||
title: "New Proxied Page",
|
||||
url: "https://duplicate-example-com.proxy.example.com/path"
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].matchedIdentifiers.url, "https://duplicate.example.com/path");
|
||||
});
|
||||
|
||||
it("should chunk large URL duplicate lookups", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("webpage");
|
||||
item.setField("title", "Existing Large Lookup Page");
|
||||
item.setField("url", "https://example.com/target-large-lookup");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let urls = [];
|
||||
for (let i = 0; i < Zotero.DB.MAX_BOUND_PARAMETERS + 5; i++) {
|
||||
urls.push(`https://example.com/not-${i}`);
|
||||
}
|
||||
urls.push("https://example.com/target-large-lookup");
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: { url: urls }
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
});
|
||||
|
||||
it("should chunk large DOI duplicate lookups", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Large DOI Lookup Article");
|
||||
item.setField("DOI", "10.1234/target-large-doi-lookup");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let dois = [];
|
||||
for (let i = 0; i < Zotero.DB.MAX_BOUND_PARAMETERS + 5; i++) {
|
||||
dois.push(`10.1234/not-large-doi-${i}`);
|
||||
}
|
||||
dois.push("10.1234/target-large-doi-lookup");
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: { doi: dois }
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
});
|
||||
|
||||
it("should extract DOI from translated item Extra before duplicate lookup", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Extra DOI Article");
|
||||
item.setField("DOI", "10.1234/extra-doi");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: [{
|
||||
itemType: "journalArticle",
|
||||
title: "New Extra DOI Article",
|
||||
extra: "DOI: 10.1234/extra-doi"
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].matchedItemIndex, 0);
|
||||
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/extra-doi");
|
||||
});
|
||||
|
||||
it("should return every translated item index sharing an Extra DOI match", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Shared Extra DOI Article");
|
||||
item.setField("DOI", "10.1234/shared-extra-doi");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
itemType: "webpage",
|
||||
title: "First Shared Extra DOI Page",
|
||||
extra: "DOI: 10.1234/shared-extra-doi"
|
||||
},
|
||||
{
|
||||
itemType: "webpage",
|
||||
title: "Second Shared Extra DOI Page",
|
||||
extra: "DOI: 10.1234/shared-extra-doi"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].matchedItemIndex, 0);
|
||||
assert.deepEqual(data.matches[0].matchedItemIndexes, [0, 1]);
|
||||
});
|
||||
|
||||
it("should return item indexes for every matching DOI stored on one item", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Article with Two DOI Values");
|
||||
item.setField("DOI", "10.1234/primary-doi");
|
||||
item.setField("extra", "DOI: 10.1234/extra-doi");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
itemType: "webpage",
|
||||
title: "Translated Extra DOI Page",
|
||||
extra: "DOI: 10.1234/extra-doi"
|
||||
},
|
||||
{
|
||||
itemType: "journalArticle",
|
||||
title: "Translated Primary DOI Article",
|
||||
DOI: "10.1234/primary-doi"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/primary-doi");
|
||||
assert.equal(data.matches[0].matchedItemIndex, 1);
|
||||
assert.deepEqual(data.matches[0].matchedItemIndexes, [0, 1]);
|
||||
});
|
||||
|
||||
it("should match saved DOI values with resolver prefixes", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing Prefixed DOI Article");
|
||||
item.setField("DOI", "https://doi.org/10.1234/prefixed-doi");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
doi: ["10.1234/prefixed-doi"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/prefixed-doi");
|
||||
});
|
||||
|
||||
it("should match existing item DOI values stored in Extra", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("webpage");
|
||||
item.setField("title", "Existing Extra DOI Page");
|
||||
item.setField("extra", "DOI: 10.1234/existing-extra-doi");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
doi: ["10.1234/existing-extra-doi"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/existing-extra-doi");
|
||||
});
|
||||
|
||||
it("should filter DOI lookup candidates before parsing values", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let unrelatedDOIItem = new Zotero.Item("journalArticle");
|
||||
unrelatedDOIItem.setField("title", "Unrelated DOI Article");
|
||||
unrelatedDOIItem.setField("DOI", "10.9999/unrelated-doi-candidate");
|
||||
await unrelatedDOIItem.saveTx();
|
||||
|
||||
let unrelatedExtraItem = new Zotero.Item("webpage");
|
||||
unrelatedExtraItem.setField("title", "Unrelated Extra Page");
|
||||
unrelatedExtraItem.setField("extra", "UNRELATED_EXTRA_SHOULD_NOT_PARSE");
|
||||
await unrelatedExtraItem.saveTx();
|
||||
|
||||
let doiItem = new Zotero.Item("journalArticle");
|
||||
doiItem.setField("title", "Existing Filtered DOI Article");
|
||||
doiItem.setField("DOI", "https://doi.org/10.1234/filtered-doi-candidate");
|
||||
await doiItem.saveTx();
|
||||
|
||||
let extraItem = new Zotero.Item("webpage");
|
||||
extraItem.setField("title", "Existing Filtered Extra Page");
|
||||
extraItem.setField("extra", "DOI: 10.1234/filtered-extra-candidate");
|
||||
await extraItem.saveTx();
|
||||
Zotero.Items.unload([unrelatedDOIItem.id, unrelatedExtraItem.id, doiItem.id, extraItem.id]);
|
||||
|
||||
let originalCleanDOI = Zotero.Utilities.cleanDOI;
|
||||
let originalExtractExtraFields = Zotero.Utilities.Internal.extractExtraFields;
|
||||
let cleanedUnrelatedDOI = false;
|
||||
let parsedUnrelatedExtra = false;
|
||||
sinon.stub(Zotero.Utilities, "cleanDOI").callsFake(function (doi) {
|
||||
if (doi && doi.includes("unrelated-doi-candidate")) {
|
||||
cleanedUnrelatedDOI = true;
|
||||
}
|
||||
return originalCleanDOI.apply(this, arguments);
|
||||
});
|
||||
sinon.stub(Zotero.Utilities.Internal, "extractExtraFields").callsFake(function (extra) {
|
||||
if (extra && extra.includes("UNRELATED_EXTRA_SHOULD_NOT_PARSE")) {
|
||||
parsedUnrelatedExtra = true;
|
||||
}
|
||||
return originalExtractExtraFields.apply(this, arguments);
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
doi: [
|
||||
"10.1234/filtered-doi-candidate",
|
||||
"10.1234/filtered-extra-candidate"
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.sameMembers(data.matches.map(match => match.id), [doiItem.id, extraItem.id]);
|
||||
assert.isFalse(cleanedUnrelatedDOI);
|
||||
assert.isFalse(parsedUnrelatedExtra);
|
||||
}
|
||||
finally {
|
||||
Zotero.Utilities.cleanDOI.restore();
|
||||
Zotero.Utilities.Internal.extractExtraFields.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("should ignore free-text Extra DOI candidates without failing the lookup", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let freeTextExtraItem = new Zotero.Item("webpage");
|
||||
freeTextExtraItem.setField("title", "Free Text Extra Page");
|
||||
freeTextExtraItem.setField("extra", "See https://doi.org/10.1234/free-text-extra for related work");
|
||||
await freeTextExtraItem.saveTx();
|
||||
|
||||
let doiItem = new Zotero.Item("journalArticle");
|
||||
doiItem.setField("title", "Existing DOI Article");
|
||||
doiItem.setField("DOI", "10.1234/free-text-extra");
|
||||
await doiItem.saveTx();
|
||||
Zotero.Items.unload([freeTextExtraItem.id, doiItem.id]);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
doi: ["10.1234/free-text-extra"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.sameMembers(data.matches.map(match => match.id), [doiItem.id]);
|
||||
});
|
||||
|
||||
it("should keep DOI lookup working when proxied item URL normalization fails", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing DOI With Bad URL Payload");
|
||||
item.setField("DOI", "10.1234/bad-proxy-url");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
proxy: {
|
||||
scheme: "https://%h.proxy.example.com/%p"
|
||||
},
|
||||
items: [{
|
||||
itemType: "journalArticle",
|
||||
title: "New DOI With Bad URL Payload",
|
||||
DOI: "10.1234/bad-proxy-url",
|
||||
url: "/relative/path"
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
});
|
||||
|
||||
it("should fall back to the current save target when requested collection no longer exists", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let collection = await createDataObject('collection');
|
||||
let targetID = collection.treeViewID;
|
||||
await collection.eraseTx();
|
||||
|
||||
let item = new Zotero.Item("journalArticle");
|
||||
item.setField("title", "Existing User Library Article");
|
||||
item.setField("DOI", "10.1234/deleted-target");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
target: targetID,
|
||||
identifiers: {
|
||||
doi: ["10.1234/deleted-target"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].libraryID, Zotero.Libraries.userLibraryID);
|
||||
});
|
||||
|
||||
it("should return display titles for base-mapped title fields", async function () {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
let item = new Zotero.Item("case");
|
||||
item.setField("caseName", "Existing Case Name");
|
||||
item.setField("url", "https://example.com/case");
|
||||
await item.saveTx();
|
||||
Zotero.Items.unload(item.id);
|
||||
|
||||
let response = await httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifiers: {
|
||||
url: ["https://example.com/case"]
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let data = JSON.parse(response.response);
|
||||
assert.lengthOf(data.matches, 1);
|
||||
assert.equal(data.matches[0].id, item.id);
|
||||
assert.equal(data.matches[0].title, "Existing Case Name");
|
||||
});
|
||||
|
||||
it("should reject requests without identifiers", async function () {
|
||||
let error = await getPromiseError(httpRequest(
|
||||
"POST",
|
||||
connectorServerPath + "/connector/findExistingItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
}
|
||||
));
|
||||
assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
|
||||
assert.include(error.message, "400");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/connector/saveSingleFile", function () {
|
||||
it("should save a webpage item with /saveSnapshot", async function () {
|
||||
var collection = await createDataObject('collection');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue