diff --git a/chrome/content/zotero/xpcom/server/server_localAPI.js b/chrome/content/zotero/xpcom/server/server_localAPI.js index 6cfbaf8247..b130b1409f 100644 --- a/chrome/content/zotero/xpcom/server/server_localAPI.js +++ b/chrome/content/zotero/xpcom/server/server_localAPI.js @@ -1966,6 +1966,14 @@ function makeNewObject(libraryID, kind, itemType) { return obj; } +async function makeNewObjectWithKey(libraryID, kind, itemType, key) { + let obj = makeNewObject(libraryID, kind, itemType); + obj.key = key; + // Setting the key identifies the object. Initialize its data before fromJSON(). + await obj.loadPrimaryData(); + return obj; +} + /** * Apply JSON to an existing or new data object. Throws HTTPError on validation * failure. Caller is responsible for calling saveTx(). @@ -2077,8 +2085,7 @@ async function writeMultipleObjects(endpoint, requestData, kind) { throw new HTTPError(404, `${kind} doesn't exist ` + `(expected version ${parseInt(entry.version)}; use 0 instead)`); } - obj = makeNewObject(libraryID, kind, entry.itemType); - obj.key = providedKey; + obj = await makeNewObjectWithKey(libraryID, kind, entry.itemType, providedKey); } } else { @@ -2202,8 +2209,7 @@ async function writeSingleObject(endpoint, requestData, kind, isPatch) { return [400, 'text/plain', 'itemType property not provided']; } try { - obj = makeNewObject(libraryID, kind, body.itemType); - obj.key = key; + obj = await makeNewObjectWithKey(libraryID, kind, body.itemType, key); } catch (e) { if (e instanceof HTTPError) { diff --git a/test/tests/server_localAPITest.js b/test/tests/server_localAPITest.js index 08e9df56b9..6f7fe70699 100644 --- a/test/tests/server_localAPITest.js +++ b/test/tests/server_localAPITest.js @@ -911,6 +911,49 @@ describe("Local API Server", function () { if (item) await item.eraseTx(); }); + it("should create a single item with a supplied key", async function () { + let key = Zotero.Utilities.generateObjectKey(); + let { response } = await apiPost('/users/0/items', { + body: [{ + key, + version: 0, + itemType: 'webpage', + title: 'Example Domain', + url: 'https://example.com/', + accessDate: '2026-09-20T01:30:00Z' + }] + }); + assert.isEmpty(response.failed); + assert.equal(response.success['0'], key); + let item = await Zotero.Items.getByLibraryAndKeyAsync( + Zotero.Libraries.userLibraryID, key); + assert.isOk(item); + assert.equal(item.getField('title'), 'Example Domain'); + assert.equal(item.getField('url'), 'https://example.com/'); + await item.eraseTx(); + }); + + it("should create a child item with a supplied parent key", async function () { + let key = Zotero.Utilities.generateObjectKey(); + let { response } = await apiPost('/users/0/items', { + body: [ + { key, version: 0, itemType: 'book', title: 'Parent' }, + { itemType: 'note', parentItem: key, note: '

Child note

' } + ] + }); + assert.isEmpty(response.failed); + assert.equal(response.success['0'], key); + let parent = await Zotero.Items.getByLibraryAndKeyAsync( + Zotero.Libraries.userLibraryID, key); + let child = await Zotero.Items.getByLibraryAndKeyAsync( + Zotero.Libraries.userLibraryID, response.success['1']); + assert.isOk(parent); + assert.isOk(child); + assert.equal(child.parentItemKey, key); + await child.eraseTx(); + await parent.eraseTx(); + }); + it("should create multiple items in one request", async function () { let { response } = await apiPost('/users/0/items', { body: [ @@ -1145,6 +1188,19 @@ describe("Local API Server", function () { await item.eraseTx(); }); + it("should create an item with a supplied key", async function () { + let key = Zotero.Utilities.generateObjectKey(); + let { status } = await apiPut(`/users/0/items/${key}`, { + body: { itemType: 'webpage', title: 'PUT Created Item' } + }); + assert.equal(status, 204); + let item = await Zotero.Items.getByLibraryAndKeyAsync( + Zotero.Libraries.userLibraryID, key); + assert.isOk(item); + assert.equal(item.getField('title'), 'PUT Created Item'); + await item.eraseTx(); + }); + it("should reject without version or If-Unmodified-Since-Version", async function () { let item = await createDataObject('item'); let { status } = await apiPut(`/users/0/items/${item.key}`, { @@ -1368,6 +1424,20 @@ describe("Local API Server", function () { await col.eraseTx(); }); + it("should create a collection with a supplied key", async function () { + let key = Zotero.Utilities.generateObjectKey(); + let { response } = await apiPost('/users/0/collections', { + body: [{ key, version: 0, name: 'Keyed Collection' }] + }); + assert.isEmpty(response.failed); + assert.equal(response.success['0'], key); + let collection = Zotero.Collections.getByLibraryAndKey( + Zotero.Libraries.userLibraryID, key); + assert.isOk(collection); + assert.equal(collection.name, 'Keyed Collection'); + await collection.eraseTx(); + }); + it("should set parentCollection when provided", async function () { let parent = await createDataObject('collection'); let { response } = await apiPost('/users/0/collections', { @@ -1447,6 +1517,20 @@ describe("Local API Server", function () { await search.eraseTx(); }); + it("should create a saved search with a supplied key", async function () { + let key = Zotero.Utilities.generateObjectKey(); + let { response } = await apiPost('/users/0/searches', { + body: [{ key, version: 0, name: 'Keyed Search', conditions: [] }] + }); + assert.isEmpty(response.failed); + assert.equal(response.success['0'], key); + let search = Zotero.Searches.getByLibraryAndKey( + Zotero.Libraries.userLibraryID, key); + assert.isOk(search); + assert.equal(search.name, 'Keyed Search'); + await search.eraseTx(); + }); + it("should delete a search", async function () { let search = await createDataObject('search', { name: 'To Delete' }); let { status } = await apiDelete(`/users/0/searches/${search.key}`, {