Local API: Fix object writes with API-client-supplied keys

Requests that included a key got a 400 with `'primaryData' not
loaded for item`
This commit is contained in:
Dan Stillman 2026-09-23 12:41:29 -04:00
parent 4e3b208e4a
commit 22a26bb483
2 changed files with 94 additions and 4 deletions

View file

@ -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) {

View file

@ -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: '<p>Child note</p>' }
]
});
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}`, {