{"record":{"id":"e0188f9309a00fdc","repo":"actualbudget/actual","slug":"cleanup-group-name-cannot-be-empty","errorCode":null,"errorMessage":"Cleanup group name cannot be empty","messagePattern":"Cleanup group name cannot be empty","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/budget/cleanup-groups.ts","lineNumber":8,"sourceCode":"import { v4 as uuidv4 } from 'uuid';\n\nimport * as db from '#server/db';\n\nexport async function resolveCleanupGroup(name: string): Promise<string> {\n  const trimmed = name.trim();\n  if (trimmed.length === 0) {\n    throw new Error('Cleanup group name cannot be empty');\n  }\n  const key = trimmed.toLowerCase();\n  const existing = await db.first<{ id: string; tombstone: 0 | 1 }>(\n    `SELECT id, tombstone FROM cleanup_groups WHERE lower(name) = ? LIMIT 1`,\n    [key],\n  );\n  if (existing) {\n    if (existing.tombstone) {\n      await db.update('cleanup_groups', { id: existing.id, tombstone: 0 });\n    }\n    return existing.id;\n  }\n  const id = uuidv4();\n  await db.insertWithSchema('cleanup_groups', {\n    id,\n    name: trimmed,\n    tombstone: 0,\n  });","sourceCodeStart":1,"sourceCodeEnd":26,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/budget/cleanup-groups.ts#L1-L26","documentation":"resolveCleanupGroup normalizes a cleanup-group name for budget cleanup templates and rejects names that are empty after trimming. Cleanup groups must have a non-empty, unique (case-insensitive) name so rows can reference them. This guard prevents creating or resolving an unnamed group in the cleanup_groups table.","triggerScenarios":"Calling resolveCleanupGroup('') or resolveCleanupGroup('   '), or a template/automation path that derives the group name from user input that ends up empty (e.g. `#cleanup group:` with nothing after the colon).","commonSituations":"Malformed cleanup template note with a missing group name, a form field left blank in tooling that calls the API, or string splitting that yields an empty token.","solutions":["Provide a non-empty group name, e.g. resolveCleanupGroup('Groceries cap').","Fix the template line to include the group name after the keyword.","Trim/validate user input before calling; reject empty names in your UI or script.","If the name comes from parsing, check the split logic isn't dropping the value."],"exampleFix":"// before\nawait resolveCleanupGroup(groupNameFromNote); // '' when note is '#cleanup group:'\n// after\nif (!groupNameFromNote?.trim()) throw new Error('Cleanup group name required');\nawait resolveCleanupGroup(groupNameFromNote);","handlingStrategy":"validation","validationCode":"function isValidGroupName(name: unknown): name is string {\n  return typeof name === 'string' && name.trim().length > 0;\n}\nif (!isValidGroupName(rawName)) {\n  throw new Error('A non-empty cleanup group name is required');\n}\nawait resolveCleanupGroup(rawName);","typeGuard":"function isNonEmptyString(v: unknown): v is string {\n  return typeof v === 'string' && v.trim().length > 0;\n}","tryCatchPattern":"try {\n  const groupId = await resolveCleanupGroup(name);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Cleanup group name cannot be empty') {\n    logger.warn('Skipping cleanup rule with empty group name');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Trim and check group names in UI/forms before submitting","Require a value after 'group' keywords in cleanup template notes","Check string-splitting logic that extracts group names from notes","Reject empty names early with clear user-facing messages"],"tags":["validation","budget-cleanup","input"],"backgroundTag":"empty-required-value","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}