What happened?
api.updateCategory(id, fields) throws a TypeError for any partial update that does not include name, even though fields is documented and typed as a partial.
I was trying to move a category into a different group during a category restructure, without changing its name.
- Expected: the category moves to the new group, name untouched.
- Actual:
TypeError: Cannot read properties of undefined (reading 'trim')
The same shape breaks a hidden-only update, and any other single-field update that omits name.
Sending the current name unchanged alongside the real change succeeds, which is the workaround I'm using:
await api.updateCategory(categoryId, { group_id: groupId, name: currentName }); // works
How can we reproduce the issue?
- Open any budget file via
@actual-app/api (api.init(...) then api.downloadBudget(...)).
- Get a valid category id and a valid category-group id, e.g. from
await api.getCategories() and await api.getCategoryGroups().
- Call
updateCategory with a partial that omits name:
await api.updateCategory(categoryId, { group_id: groupId });
- Expected: resolves; the category is now in the new group with its name unchanged.
Actual: rejects with TypeError: Cannot read properties of undefined (reading 'trim').
- Confirm it is the omission of
name that matters, by re-running with the name included — this succeeds:
await api.updateCategory(categoryId, { group_id: groupId, name: currentName });
Nothing is written when it throws: the category is left untouched.
Root cause
updateCategory normalizes name unconditionally, before checking whether it was supplied:
async function updateCategory$1(category) {
try {
await updateCategory$2(categoryModel$1.toDb({
...category,
name: category.name.trim() // <-- throws when `name` was not passed
}));
The public entry point preserves the absence of the key, so name really is undefined by the time it reaches the trim:
api.updateCategory(id, fields) → handlers["api/category-update"] → { id, ...categoryModel.fromExternal(fields) } → handlers["category-update"] → updateCategory$1.
Verified present in 26.8.1 and still present in 26.9.0 (dist/index.js:118200).
Scope
Category-only — I checked the two sibling handlers and neither has the pattern:
handlers["api/payee-update"] → payeeModel.fromExternal(fields), no unconditional name access
updateCategoryGroup$1 → categoryGroupModel$1.toDb(group), no trim
Suggested fix
Only normalize name when it is present, and build the update payload from the keys actually passed rather than a fixed field list:
await updateCategory$2(categoryModel$1.toDb({
...category,
...(typeof category.name === 'string' ? { name: category.name.trim() } : {}),
}));
Suggested tests
- update
group_id only → succeeds, name unchanged
- update
hidden only → succeeds, name unchanged
- update
name only → succeeds, group unchanged
- update with empty
fields {} → clean validation error, not a TypeError
Where are you hosting Actual?
Docker (self-hosted, actualbudget/actual-server 26.8.1)
Operating System
Linux
Additional environment
@actual-app/api 26.8.1; the offending line also confirmed by inspection in 26.9.0
- Reached via the API, not the web UI, so no browser is involved
What happened?
api.updateCategory(id, fields)throws aTypeErrorfor any partial update that does not includename, even thoughfieldsis documented and typed as a partial.I was trying to move a category into a different group during a category restructure, without changing its name.
TypeError: Cannot read properties of undefined (reading 'trim')The same shape breaks a
hidden-only update, and any other single-field update that omitsname.Sending the current name unchanged alongside the real change succeeds, which is the workaround I'm using:
How can we reproduce the issue?
@actual-app/api(api.init(...)thenapi.downloadBudget(...)).await api.getCategories()andawait api.getCategoryGroups().updateCategorywith a partial that omitsname:Actual: rejects with
TypeError: Cannot read properties of undefined (reading 'trim').namethat matters, by re-running with the name included — this succeeds:Nothing is written when it throws: the category is left untouched.
Root cause
updateCategorynormalizesnameunconditionally, before checking whether it was supplied:The public entry point preserves the absence of the key, so
namereally isundefinedby the time it reaches the trim:api.updateCategory(id, fields)→handlers["api/category-update"]→{ id, ...categoryModel.fromExternal(fields) }→handlers["category-update"]→updateCategory$1.Verified present in 26.8.1 and still present in 26.9.0 (
dist/index.js:118200).Scope
Category-only — I checked the two sibling handlers and neither has the pattern:
handlers["api/payee-update"]→payeeModel.fromExternal(fields), no unconditional name accessupdateCategoryGroup$1→categoryGroupModel$1.toDb(group), no trimSuggested fix
Only normalize
namewhen it is present, and build the update payload from the keys actually passed rather than a fixed field list:Suggested tests
group_idonly → succeeds, name unchangedhiddenonly → succeeds, name unchangednameonly → succeeds, group unchangedfields{}→ clean validation error, not aTypeErrorWhere are you hosting Actual?
Docker (self-hosted,
actualbudget/actual-server26.8.1)Operating System
Linux
Additional environment
@actual-app/api26.8.1; the offending line also confirmed by inspection in 26.9.0