Remove old Author object & fix issue deleting empty authors

This commit is contained in:
advplyr 2024-08-31 13:27:48 -05:00
parent acc4bdbc5e
commit ba742563c2
13 changed files with 227 additions and 314 deletions

View File

@ -462,26 +462,6 @@ class Database {
await this.models.series.removeById(seriesId)
}
async createAuthor(oldAuthor) {
if (!this.sequelize) return false
await this.models.author.createFromOld(oldAuthor)
}
async createBulkAuthors(oldAuthors) {
if (!this.sequelize) return false
await this.models.author.createBulkFromOld(oldAuthors)
}
updateAuthor(oldAuthor) {
if (!this.sequelize) return false
return this.models.author.updateFromOld(oldAuthor)
}
async removeAuthor(authorId) {
if (!this.sequelize) return false
await this.models.author.removeById(authorId)
}
async createBulkBookAuthors(bookAuthors) {
if (!this.sequelize) return false
await this.models.bookAuthor.bulkCreate(bookAuthors)
@ -684,7 +664,7 @@ class Database {
*/
async getAuthorIdByName(libraryId, authorName) {
if (!this.libraryFilterData[libraryId]) {
return (await this.authorModel.getOldByNameAndLibrary(authorName, libraryId))?.id || null
return (await this.authorModel.getByNameAndLibrary(authorName, libraryId))?.id || null
}
return this.libraryFilterData[libraryId].authors.find((au) => au.name === authorName)?.id || null
}

View File

@ -21,6 +21,11 @@ const naturalSort = createNewSortInstance({
* @property {import('../models/User')} user
*
* @typedef {Request & RequestUserObject} RequestWithUser
*
* @typedef RequestEntityObject
* @property {import('../models/Author')} author
*
* @typedef {RequestWithUser & RequestEntityObject} AuthorControllerRequest
*/
class AuthorController {
@ -29,13 +34,13 @@ class AuthorController {
/**
* GET: /api/authors/:id
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async findOne(req, res) {
const include = (req.query.include || '').split(',')
const authorJson = req.author.toJSON()
const authorJson = req.author.toOldJSON()
// Used on author landing page to include library items and items grouped in series
if (include.includes('items')) {
@ -80,25 +85,30 @@ class AuthorController {
/**
* PATCH: /api/authors/:id
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
const payload = req.body
let hasUpdated = false
// author imagePath must be set through other endpoints as of v2.4.5
if (payload.imagePath !== undefined) {
Logger.warn(`[AuthorController] Updating local author imagePath is not supported`)
delete payload.imagePath
const keysToUpdate = ['name', 'description', 'asin']
const payload = {}
for (const key in req.body) {
if (keysToUpdate.includes(key) && (typeof req.body[key] === 'string' || req.body[key] === null)) {
payload[key] = req.body[key]
}
}
if (!Object.keys(payload).length) {
Logger.error(`[AuthorController] Invalid request payload. No valid keys found`, req.body)
return res.status(400).send('Invalid request payload. No valid keys found')
}
let hasUpdated = false
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
// Check if author name matches another author and merge the authors
let existingAuthor = null
if (authorNameUpdate) {
const author = await Database.authorModel.findOne({
existingAuthor = await Database.authorModel.findOne({
where: {
id: {
[sequelize.Op.not]: req.author.id
@ -106,7 +116,6 @@ class AuthorController {
name: payload.name
}
})
existingAuthor = author?.getOldAuthor()
}
if (existingAuthor) {
Logger.info(`[AuthorController] Merging author "${req.author.name}" with "${existingAuthor.name}"`)
@ -143,86 +152,87 @@ class AuthorController {
}
// Remove old author
await Database.removeAuthor(req.author.id)
SocketAuthority.emitter('author_removed', req.author.toJSON())
const oldAuthorJSON = req.author.toOldJSON()
await req.author.destroy()
SocketAuthority.emitter('author_removed', oldAuthorJSON)
// Update filter data
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
Database.removeAuthorFromFilterData(oldAuthorJSON.libraryId, oldAuthorJSON.id)
// Send updated num books for merged author
const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthor.id)
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
SocketAuthority.emitter('author_updated', existingAuthor.toOldJSONExpanded(numBooks))
res.json({
author: existingAuthor.toJSON(),
author: existingAuthor.toOldJSON(),
merged: true
})
} else {
// Regular author update
if (req.author.update(payload)) {
hasUpdated = true
}
return
}
if (hasUpdated) {
req.author.updatedAt = Date.now()
// Regular author update
req.author.set(payload)
if (req.author.changed()) {
await req.author.save()
hasUpdated = true
}
let numBooksForAuthor = 0
if (authorNameUpdate) {
const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
if (hasUpdated) {
let numBooksForAuthor = 0
if (authorNameUpdate) {
const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
numBooksForAuthor = allItemsWithAuthor.length
const oldLibraryItems = []
// Update author name on all books
for (const libraryItem of allItemsWithAuthor) {
libraryItem.media.authors = libraryItem.media.authors.map((au) => {
if (au.id === req.author.id) {
au.name = req.author.name
}
return au
})
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
oldLibraryItems.push(oldLibraryItem)
numBooksForAuthor = allItemsWithAuthor.length
const oldLibraryItems = []
// Update author name on all books
for (const libraryItem of allItemsWithAuthor) {
libraryItem.media.authors = libraryItem.media.authors.map((au) => {
if (au.id === req.author.id) {
au.name = req.author.name
}
return au
})
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
oldLibraryItems.push(oldLibraryItem)
await libraryItem.saveMetadataFile()
}
if (oldLibraryItems.length) {
SocketAuthority.emitter(
'items_updated',
oldLibraryItems.map((li) => li.toJSONExpanded())
)
}
} else {
numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
await libraryItem.saveMetadataFile()
}
await Database.updateAuthor(req.author)
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooksForAuthor))
if (oldLibraryItems.length) {
SocketAuthority.emitter(
'items_updated',
oldLibraryItems.map((li) => li.toJSONExpanded())
)
}
} else {
numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
}
res.json({
author: req.author.toJSON(),
updated: hasUpdated
})
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooksForAuthor))
}
res.json({
author: req.author.toOldJSON(),
updated: hasUpdated
})
}
/**
* DELETE: /api/authors/:id
* Remove author from all books and delete
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async delete(req, res) {
Logger.info(`[AuthorController] Removing author "${req.author.name}"`)
await Database.authorModel.removeById(req.author.id)
if (req.author.imagePath) {
await CacheManager.purgeImageCache(req.author.id) // Purge cache
}
SocketAuthority.emitter('author_removed', req.author.toJSON())
await req.author.destroy()
SocketAuthority.emitter('author_removed', req.author.toOldJSON())
// Update filter data
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
@ -234,7 +244,7 @@ class AuthorController {
* POST: /api/authors/:id/image
* Upload author image from web URL
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async uploadImage(req, res) {
@ -265,13 +275,14 @@ class AuthorController {
}
req.author.imagePath = result.path
req.author.updatedAt = Date.now()
await Database.authorModel.updateFromOld(req.author)
// imagePath may not have changed, but we still want to update the updatedAt field to bust image cache
req.author.changed('imagePath', true)
await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
res.json({
author: req.author.toJSON()
author: req.author.toOldJSON()
})
}
@ -279,7 +290,7 @@ class AuthorController {
* DELETE: /api/authors/:id/image
* Remove author image & delete image file
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async deleteImage(req, res) {
@ -291,19 +302,19 @@ class AuthorController {
await CacheManager.purgeImageCache(req.author.id) // Purge cache
await CoverManager.removeFile(req.author.imagePath)
req.author.imagePath = null
await Database.authorModel.updateFromOld(req.author)
await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
res.json({
author: req.author.toJSON()
author: req.author.toOldJSON()
})
}
/**
* POST: /api/authors/:id/match
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async match(req, res) {
@ -342,24 +353,22 @@ class AuthorController {
}
if (hasUpdates) {
req.author.updatedAt = Date.now()
await Database.updateAuthor(req.author)
await req.author.save()
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks))
}
res.json({
updated: hasUpdates,
author: req.author
author: req.author.toOldJSON()
})
}
/**
* GET: /api/authors/:id/image
*
* @param {RequestWithUser} req
* @param {AuthorControllerRequest} req
* @param {Response} res
*/
async getImage(req, res) {
@ -392,7 +401,7 @@ class AuthorController {
* @param {NextFunction} next
*/
async middleware(req, res, next) {
const author = await Database.authorModel.getOldById(req.params.id)
const author = await Database.authorModel.findByPk(req.params.id)
if (!author) return res.sendStatus(404)
if (req.method == 'DELETE' && !req.user.canDelete) {

View File

@ -887,8 +887,7 @@ class LibraryController {
const oldAuthors = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.books.length
const oldAuthor = author.toOldJSONExpanded(author.books.length)
oldAuthor.lastFirst = author.lastFirst
oldAuthors.push(oldAuthor)
}

View File

@ -151,6 +151,8 @@ class LibraryItemController {
* PATCH: /items/:id/media
* Update media for a library item. Will create new authors & series when necessary
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
@ -185,6 +187,12 @@ class LibraryItemController {
seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
}
let authorsRemoved = []
if (libraryItem.isBook && mediaPayload.metadata?.authors) {
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
}
const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
if (hasUpdates) {
libraryItem.updatedAt = Date.now()
@ -205,6 +213,15 @@ class LibraryItemController {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
if (authorsRemoved.length) {
// Check remove empty authors
Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`)
await this.checkRemoveAuthorsWithNoBooks(
libraryItem.libraryId,
authorsRemoved.map((au) => au.id)
)
}
}
res.json({
updated: hasUpdates,
@ -823,7 +840,7 @@ class LibraryItemController {
// We actually need to check for Webkit on Apple mobile devices because this issue impacts all browsers on iOS/iPadOS/etc, not just Safari.
const isAppleMobileBrowser = ua.device.vendor === 'Apple' && ua.device.type === 'mobile' && ua.engine.name === 'WebKit'
if (isAppleMobileBrowser && audioMimeType === AudioMimeType.M4B) {
audioMimeType = 'audio/m4b'
audioMimeType = 'audio/m4b'
}
res.setHeader('Content-Type', audioMimeType)
}

View File

@ -124,6 +124,13 @@ class CacheManager {
await this.ensureCachePaths()
}
/**
*
* @param {import('express').Response} res
* @param {import('../models/Author')} author
* @param {{ format?: string, width?: number, height?: number }} options
* @returns
*/
async handleAuthorCache(res, author, options = {}) {
const format = options.format || 'webp'
const width = options.width || 400

View File

@ -1,7 +1,5 @@
const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldAuthor = require('../objects/entities/Author')
class Author extends Model {
constructor(values, options) {
super(values, options)
@ -26,69 +24,6 @@ class Author extends Model {
this.createdAt
}
getOldAuthor() {
return new oldAuthor({
id: this.id,
asin: this.asin,
name: this.name,
description: this.description,
imagePath: this.imagePath,
libraryId: this.libraryId,
addedAt: this.createdAt.valueOf(),
updatedAt: this.updatedAt.valueOf()
})
}
static updateFromOld(oldAuthor) {
const author = this.getFromOld(oldAuthor)
return this.update(author, {
where: {
id: author.id
}
})
}
static createFromOld(oldAuthor) {
const author = this.getFromOld(oldAuthor)
return this.create(author)
}
static createBulkFromOld(oldAuthors) {
const authors = oldAuthors.map(this.getFromOld)
return this.bulkCreate(authors)
}
static getFromOld(oldAuthor) {
return {
id: oldAuthor.id,
name: oldAuthor.name,
lastFirst: oldAuthor.lastFirst,
asin: oldAuthor.asin,
description: oldAuthor.description,
imagePath: oldAuthor.imagePath,
libraryId: oldAuthor.libraryId
}
}
static removeById(authorId) {
return this.destroy({
where: {
id: authorId
}
})
}
/**
* Get oldAuthor by id
* @param {string} authorId
* @returns {Promise<oldAuthor>}
*/
static async getOldById(authorId) {
const author = await this.findByPk(authorId)
if (!author) return null
return author.getOldAuthor()
}
/**
* Check if author exists
* @param {string} authorId
@ -99,25 +34,22 @@ class Author extends Model {
}
/**
* Get old author by name and libraryId. name case insensitive
* Get author by name and libraryId. name case insensitive
* TODO: Look for authors ignoring punctuation
*
* @param {string} authorName
* @param {string} libraryId
* @returns {Promise<oldAuthor>}
* @returns {Promise<Author>}
*/
static async getOldByNameAndLibrary(authorName, libraryId) {
const author = (
await this.findOne({
where: [
where(fn('lower', col('name')), authorName.toLowerCase()),
{
libraryId
}
]
})
)?.getOldAuthor()
return author
static async getByNameAndLibrary(authorName, libraryId) {
return this.findOne({
where: [
where(fn('lower', col('name')), authorName.toLowerCase()),
{
libraryId
}
]
})
}
/**
@ -213,5 +145,36 @@ class Author extends Model {
})
Author.belongsTo(library)
}
toOldJSON() {
return {
id: this.id,
asin: this.asin,
name: this.name,
description: this.description,
imagePath: this.imagePath,
libraryId: this.libraryId,
addedAt: this.createdAt.valueOf(),
updatedAt: this.updatedAt.valueOf()
}
}
/**
*
* @param {number} numBooks
* @returns
*/
toOldJSONExpanded(numBooks = 0) {
const oldJson = this.toOldJSON()
oldJson.numBooks = numBooks
return oldJson
}
toJSONMinimal() {
return {
id: this.id,
name: this.name
}
}
}
module.exports = Author

View File

@ -773,7 +773,7 @@ class LibraryItem extends Model {
/**
* Get book library items for author, optional use user permissions
* @param {oldAuthor} author
* @param {import('./Author')} author
* @param {import('./User')} user
* @returns {Promise<oldLibraryItem[]>}
*/

View File

@ -1,101 +0,0 @@
const Logger = require('../../Logger')
const uuidv4 = require("uuid").v4
const { checkNamesAreEqual, nameToLastFirst } = require('../../utils/parsers/parseNameString')
class Author {
constructor(author) {
this.id = null
this.asin = null
this.name = null
this.description = null
this.imagePath = null
this.addedAt = null
this.updatedAt = null
this.libraryId = null
if (author) {
this.construct(author)
}
}
construct(author) {
this.id = author.id
this.asin = author.asin
this.name = author.name || ''
this.description = author.description || null
this.imagePath = author.imagePath
this.addedAt = author.addedAt
this.updatedAt = author.updatedAt
this.libraryId = author.libraryId
}
get lastFirst() {
if (!this.name) return ''
return nameToLastFirst(this.name)
}
toJSON() {
return {
id: this.id,
asin: this.asin,
name: this.name,
description: this.description,
imagePath: this.imagePath,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
libraryId: this.libraryId
}
}
toJSONExpanded(numBooks = 0) {
const json = this.toJSON()
json.numBooks = numBooks
return json
}
toJSONMinimal() {
return {
id: this.id,
name: this.name
}
}
setData(data, libraryId) {
this.id = uuidv4()
if (!data.name) {
Logger.error(`[Author] setData: Setting author data without a name`, data)
}
this.name = data.name || ''
this.description = data.description || null
this.asin = data.asin || null
this.imagePath = data.imagePath || null
this.addedAt = Date.now()
this.updatedAt = Date.now()
this.libraryId = libraryId
}
update(payload) {
const json = this.toJSON()
delete json.id
delete json.addedAt
delete json.updatedAt
let hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined && json[key] != payload[key]) {
this[key] = payload[key]
hasUpdates = true
}
}
return hasUpdates
}
checkNameEquals(name) {
if (!name) return false
if (this.name === null) {
Logger.error(`[Author] Author name is null (${this.id})`)
return false
}
return checkNamesAreEqual(this.name, name)
}
}
module.exports = Author

View File

@ -1,5 +1,6 @@
const express = require('express')
const Path = require('path')
const sequelize = require('sequelize')
const Logger = require('../Logger')
const Database = require('../Database')
@ -32,7 +33,6 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
const MiscController = require('../controllers/MiscController')
const ShareController = require('../controllers/ShareController')
const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
class ApiRouter {
@ -469,6 +469,54 @@ class ApiRouter {
}
}
/**
* Remove authors with no books and unset asin, description and imagePath
* Note: Other implementation is in BookScanner.checkAuthorsRemovedFromBooks (can be merged)
*
* @param {string} libraryId
* @param {string[]} authorIds
* @returns {Promise<void>}
*/
async checkRemoveAuthorsWithNoBooks(libraryId, authorIds) {
if (!authorIds?.length) return
const bookAuthorsToRemove = (
await Database.authorModel.findAll({
where: [
{
id: authorIds,
asin: {
[sequelize.Op.or]: [null, '']
},
description: {
[sequelize.Op.or]: [null, '']
},
imagePath: {
[sequelize.Op.or]: [null, '']
}
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
],
attributes: ['id', 'name'],
raw: true
})
).map((au) => ({ id: au.id, name: au.name }))
if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({
where: {
id: bookAuthorsToRemove.map((au) => au.id)
}
})
bookAuthorsToRemove.forEach(({ id, name }) => {
Database.removeAuthorFromFilterData(libraryId, id)
// TODO: Clients were expecting full author in payload but its unnecessary
SocketAuthority.emitter('author_removed', { id, libraryId })
Logger.info(`[ApiRouter] Removed author "${name}" with no books`)
})
}
}
/**
* Remove an empty series & close an open RSS feed
* @param {import('../models/Series')} series
@ -567,11 +615,13 @@ class ApiRouter {
}
if (!mediaMetadata.authors[i].id) {
let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryId)
if (!author) {
author = new Author()
author.setData(mediaMetadata.authors[i], libraryId)
Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
author = await Database.authorModel.create({
name: authorName,
libraryId
})
Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
newAuthors.push(author)
// Update filter data
Database.addAuthorToFilterData(libraryId, author.name, author.id)
@ -584,10 +634,9 @@ class ApiRouter {
// Remove authors without an id
mediaMetadata.authors = mediaMetadata.authors.filter((au) => !!au.id)
if (newAuthors.length) {
await Database.createBulkAuthors(newAuthors)
SocketAuthority.emitter(
'authors_added',
newAuthors.map((au) => au.toJSON())
newAuthors.map((au) => au.toOldJSON())
)
}
}

View File

@ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
const LibraryScan = require('./LibraryScan')
const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
const LibraryScanner = require('./LibraryScanner')
const CoverManager = require('../managers/CoverManager')
@ -206,12 +205,13 @@ class Scanner {
}
const authorPayload = []
for (const authorName of matchData.author) {
let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryItem.libraryId)
let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryItem.libraryId)
if (!author) {
author = new Author()
author.setData({ name: authorName }, libraryItem.libraryId)
await Database.createAuthor(author)
SocketAuthority.emitter('author_added', author.toJSON())
author = await Database.authorModel.create({
name: authorName,
libraryId: libraryItem.libraryId
})
SocketAuthority.emitter('author_added', author.toOldJSON())
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
}

View File

@ -42,15 +42,15 @@ module.exports.parse = (nameString) => {
var splitNames = []
// Example &LF: Friedman, Milton & Friedman, Rose
if (nameString.includes('&')) {
nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else if (nameString.includes(' and ')) {
nameString.split(' and ').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
nameString.split(' and ').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else if (nameString.includes(';')) {
nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
nameString.split(';').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
} else {
splitNames = nameString.split(',')
}
if (splitNames.length) splitNames = splitNames.map(a => a.trim())
if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
var names = []
@ -84,21 +84,12 @@ module.exports.parse = (nameString) => {
}
// Filter out names that have no first and last
names = names.filter(n => n.first_name || n.last_name)
names = names.filter((n) => n.first_name || n.last_name)
// Set name strings and remove duplicates
const namesArray = [...new Set(names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name))]
const namesArray = [...new Set(names.map((a) => (a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)))]
return {
names: namesArray // Array of first last
}
}
module.exports.checkNamesAreEqual = (name1, name2) => {
if (!name1 || !name2) return false
// e.g. John H. Smith will be equal to John H Smith
name1 = String(name1).toLowerCase().trim().replace(/\./g, '')
name2 = String(name2).toLowerCase().trim().replace(/\./g, '')
return name1 === name2
}

View File

@ -73,8 +73,7 @@ module.exports = {
})
const authorMatches = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.dataValues.numBooks
const oldAuthor = author.toOldJSONExpanded(author.dataValues.numBooks)
authorMatches.push(oldAuthor)
}
return authorMatches

View File

@ -353,7 +353,7 @@ module.exports = {
return {
authors: authors.map((au) => {
const numBooks = au.books.length || 0
return au.getOldAuthor().toJSONExpanded(numBooks)
return au.toOldJSONExpanded(numBooks)
}),
count
}
@ -409,7 +409,7 @@ module.exports = {
/**
* Get library items for an author, optional use user permissions
* @param {oldAuthor} author
* @param {import('../../models/Author')} author
* @param {import('../../models/User')} user
* @param {number} limit
* @param {number} offset