handle merging tag categories
All checks were successful
Build and Push Docker Image / build (push) Successful in 55s

This commit is contained in:
Garret Patti
2026-04-19 23:25:09 -04:00
parent fc9a7af7c3
commit 71a026f01e
3 changed files with 137 additions and 5 deletions

View File

@@ -98,6 +98,62 @@ export function deleteCategoryForce(id: string): void {
if (result.changes === 0) throw new Error(`Category not found: ${id}`)
}
/**
* Merge all tags from `sourceId` category into `targetId` category, then
* delete the source category. Tags with conflicting names (case-insensitive)
* are combined: their media_tags and tag_mappings rows are re-pointed to the
* target tag, and the source tag is deleted.
*/
export function mergeCategories(sourceId: string, targetId: string): void {
const db = getDb()
const source = db.prepare('SELECT id, name FROM tag_categories WHERE id = ?').get(sourceId) as TagCategory | undefined
if (!source) throw new Error(`Source category not found: ${sourceId}`)
const target = db.prepare('SELECT id, name FROM tag_categories WHERE id = ?').get(targetId) as TagCategory | undefined
if (!target) throw new Error(`Target category not found: ${targetId}`)
const sourceTags = db
.prepare('SELECT id, name, category_id as categoryId FROM tags WHERE category_id = ?')
.all(sourceId) as Tag[]
const targetTags = db
.prepare('SELECT id, name, category_id as categoryId FROM tags WHERE category_id = ?')
.all(targetId) as Tag[]
const targetTagsByNameLower = new Map(targetTags.map((t) => [t.name.toLowerCase(), t]))
const txn = db.transaction(() => {
for (const srcTag of sourceTags) {
const conflict = targetTagsByNameLower.get(srcTag.name.toLowerCase())
if (conflict) {
// Re-point media_tags from source tag to target tag (ignore duplicates)
db.prepare(
`INSERT OR IGNORE INTO media_tags (item_key, tag_id)
SELECT item_key, ? FROM media_tags WHERE tag_id = ?`
).run(conflict.id, srcTag.id)
db.prepare('DELETE FROM media_tags WHERE tag_id = ?').run(srcTag.id)
// Re-point tag_mappings from source tag to target tag (ignore duplicates)
db.prepare(
`UPDATE OR IGNORE tag_mappings SET tag_id = ? WHERE tag_id = ?`
).run(conflict.id, srcTag.id)
// Delete any remaining (were duplicates that couldn't be updated)
db.prepare('DELETE FROM tag_mappings WHERE tag_id = ?').run(srcTag.id)
// Delete the source tag
db.prepare('DELETE FROM tags WHERE id = ?').run(srcTag.id)
} else {
// No conflict — just move the tag to the target category
db.prepare('UPDATE tags SET category_id = ? WHERE id = ?').run(targetId, srcTag.id)
}
}
// Delete the now-empty source category
db.prepare('DELETE FROM tag_categories WHERE id = ?').run(sourceId)
})
txn()
}
// ─── Tags ─────────────────────────────────────────────────────────────────────
export function getTags(categoryId?: string): Tag[] {