Fix book edit crash when a series or book link already exists

This commit is contained in:
mikiher committed 2026-11-09 19:09:36 +02:00
1 parent 49c0ef6c07
commit 9b055f80a6
3 files changed
+109 -10

No files matched your search

+23 -5
View File
@@ -469,7 +469,13 @@ class Book extends Model {
const { author, created } = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId)
// findOrCreate matches case-insensitively, so a second spelling can resolve to an author just linked
if (this.authors.some((au) => au.id === author.id)) continue
await bookAuthorModel.create({ bookId: this.id, authorId: author.id })
try {
await bookAuthorModel.create({ bookId: this.id, authorId: author.id })
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') throw error
this.authors.push(author)
continue
}
if (created) {
SocketAuthority.emitter('author_added', author.toOldJSON())
} else {
@@ -525,11 +531,23 @@ class Book extends Model {
}
} else {
const series = await seriesModel.findOrCreateByNameAndLibrary(seriesObj.name, libraryId)
series.bookSeries = await bookSeriesModel.create({ bookId: this.id, seriesId: series.id, sequence: seriesObjSequence })
try {
series.bookSeries = await bookSeriesModel.create({ bookId: this.id, seriesId: series.id, sequence: seriesObjSequence })
seriesAdded.push(series)
hasUpdates = true
Logger.debug(`[Book] "${this.title}" Added series "${series.name}"`)
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') throw error
series.bookSeries = await bookSeriesModel.findOne({ where: { bookId: this.id, seriesId: series.id } })
if (!series.bookSeries) throw error
if (series.bookSeries.sequence !== seriesObjSequence) {
series.bookSeries.sequence = seriesObjSequence
await series.bookSeries.save()
hasUpdates = true
Logger.debug(`[Book] "${this.title}" Updated series "${series.name}" sequence ${seriesObjSequence}`)
}
}
this.series.push(series)
seriesAdded.push(series)
hasUpdates = true
Logger.debug(`[Book] "${this.title}" Added series "${series.name}"`)
}
}
+15 -5
View File
@@ -75,11 +75,21 @@ class Series extends Model {
static async findOrCreateByNameAndLibrary(seriesName, libraryId) {
const series = await this.getByNameAndLibrary(seriesName, libraryId)
if (series) return series
return this.create({
name: seriesName,
nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
libraryId
})
try {
return await this.create({
name: seriesName,
nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
libraryId
})
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') throw error
// The unique index is on the exact name. lower() can miss that row.
const existingSeries = await this.findOne({
where: { name: seriesName, libraryId }
})
if (!existingSeries) throw error
return existingSeries
}
}
/**
+71
View File
@@ -0,0 +1,71 @@
const { expect } = require('chai')
const { Sequelize } = require('sequelize')
const sinon = require('sinon')
const Database = require('../../../server/Database')
const Logger = require('../../../server/Logger')
describe('Book author and series updates', () => {
/** @type {import('../../../server/models/Library')} */
let library
/** @type {import('../../../server/models/Book')} */
let book
beforeEach(async () => {
global.ServerSettings = {}
Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '')
await Database.buildModels()
sinon.stub(Logger, 'info')
sinon.stub(Logger, 'debug')
sinon.stub(Logger, 'warn')
sinon.stub(Logger, 'error')
library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' })
book = await Database.bookModel.create({ title: 'Test Book', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] })
book.authors = []
book.series = []
})
afterEach(async () => {
sinon.restore()
await Database.sequelize.sync({ force: true })
})
it('does not throw when the bookSeries link already exists', async () => {
const series = await Database.seriesModel.create({ name: 'Crescent City', libraryId: library.id })
await Database.bookSeriesModel.create({ bookId: book.id, seriesId: series.id, sequence: '3' })
const result = await book.updateSeriesFromRequest([{ name: 'Crescent City', sequence: '3' }], library.id)
expect(result.hasUpdates).to.be.false
const links = await Database.bookSeriesModel.findAll({ where: { bookId: book.id, seriesId: series.id } })
expect(links).to.have.lengthOf(1)
})
it('reuses an existing series when create hits the name unique constraint', async () => {
const series = await Database.seriesModel.create({ name: 'Crescent City', libraryId: library.id })
sinon.stub(Database.seriesModel, 'getByNameAndLibrary').resolves(null)
const result = await book.updateSeriesFromRequest([{ name: 'Crescent City', sequence: '3' }], library.id)
expect(result.seriesAdded).to.have.lengthOf(1)
expect(result.seriesAdded[0].id).to.equal(series.id)
const seriesRows = await Database.seriesModel.findAll({ where: { libraryId: library.id, name: 'Crescent City' } })
expect(seriesRows).to.have.lengthOf(1)
const links = await Database.bookSeriesModel.findAll({ where: { bookId: book.id, seriesId: series.id } })
expect(links).to.have.lengthOf(1)
})
it('does not throw when the bookAuthor link already exists', async () => {
const author = await Database.authorModel.create({ name: 'Sarah Maas', libraryId: library.id })
await Database.bookAuthorModel.create({ bookId: book.id, authorId: author.id })
const result = await book.updateAuthorsFromRequest(['Sarah Maas'], library.id)
expect(result.authorsAdded).to.have.lengthOf(0)
const links = await Database.bookAuthorModel.findAll({ where: { bookId: book.id, authorId: author.id } })
expect(links).to.have.lengthOf(1)
})
})