First full release

This commit is contained in:
Luka Romih
2023-03-10 12:50:23 +01:00
parent 257f3c354f
commit 9867875ef2
285 changed files with 10980 additions and 4692 deletions
+248 -12
View File
@@ -1,11 +1,18 @@
const ConsultancyEntry = require('../../../models/consultancy-entry')
const Domain = require('../../../models/domain')
const User = require('../../../models/user')
const { promisify } = require('util')
const i18next = require('i18next')
const {
deleteConsultancyEntryFromIndex
} = require('../../../models/search-engine')
const email = require('../../../models/email')
const helper = require('../../../models/helpers')
const { getInstanceSetting } = require('../../../models/helpers')
const { searchConsultancyEntryIndex } = require('../../../models/search-engine')
const { prepareConsultancyEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const generateQuery = require('../../../models/helpers/search/generate-query')
const consultancy = {}
@@ -23,6 +30,43 @@ consultancy.listNewEntries = async (req, res) => {
res.send(data)
}
consultancy.sendPaginationData = async (req, res) => {
let requestType = req.query.type
const isAdminPage = req.query.isAdmin === 'true'
let page = +req.query.p || 1
if (page < 1) {
page = 1
}
if (isAdminPage) {
if (
req.user &&
(req.user.hasRole('portal admin') ||
req.user.hasRole('consultancy admin'))
) {
// Everything is allowed, nothing to do here
} else if (req.user && req.user.hasRole('consultant')) {
if (!(requestType === 'in progress' || requestType === 'published')) {
return res.status(400).send()
}
} else {
// for now, public can only see published entries
requestType = 'published'
}
}
return await consultancyRequestItems(
req,
res,
requestType || 'published',
'components/consultancy/api/consultancy-item-rendered',
isAdminPage,
!isAdminPage, // -> In current implementation, it is just the inverse of isAdminPage
page
)
}
consultancy.createQuestion = async (req, res) => {
const q = req.body
const consultancyEntry = {}
@@ -35,7 +79,7 @@ consultancy.createQuestion = async (req, res) => {
const { description } = q
if (!description) {
return res.status(400).send('description is a required parameter!')
return res.status(400).send('Description is a required parameter!')
}
consultancyEntry.description = description
@@ -52,24 +96,46 @@ consultancy.createQuestion = async (req, res) => {
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
})
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
const isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
// TODO SEND EMAIL
// TODOOOOOOOOO
let domainNameSl
if (consultancyEntry.domainPrimaryIdInitial) {
domainNameSl = (
await Domain.fetchById(consultancyEntry.domainPrimaryIdInitial)
).nameSl
} else {
domainNameSl = ''
}
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
let emails
let subjectText
if (isOwnConsultancyEnabled) {
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
} else {
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
emails = await getInstanceSetting('zrc_email')
}
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
propertyToPassGoesHere: 'test1234'
nameAndSurname: `${res.locals.user.firstName} ${res.locals.user.lastName}`,
email: res.locals.user.email,
domain: domainNameSl,
institution: consultancyEntry.institution,
question: consultancyEntry.description,
existingSolutions: consultancyEntry.existingSolutions,
examplesOfUse: consultancyEntry.examplesOfUse
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
subject: subjectText,
html: emailHtml
})
/// /////////////////
res.status(201).send()
}
@@ -179,9 +245,10 @@ consultancy.assign = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-assigned')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Novo terminološko vprašanje',
subject: req.t('Novo terminološko vprašanje'),
html: emailHtml
})
@@ -234,9 +301,10 @@ consultancy.sendToReview = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-item-review')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Potrditev objave',
subject: req.t('Potrditev objave'),
html: emailHtml
})
@@ -254,6 +322,19 @@ consultancy.publish = async (req, res) => {
if (!entry.title) {
return res.status(400).send('Answer not completed')
}
const author = await User.fetchUser(entry.authorId)
const portalName = await getInstanceSetting(`portal_name_${author.language}`)
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-publish-notify', {
portalName
})
await email.send({
to: author.email,
subject: i18next.t('Objava terminološkega vprašanja', {
lng: author.language
}),
html: emailHtml
})
await ConsultancyEntry.publish(questionId, answerAuthors)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
@@ -267,7 +348,9 @@ consultancy.updateQuestion = async (req, res) => {
if (!id) return res.status(400).send({})
if (questionTitle === '' || question === '' || answer === '') {
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!')
return res
.status(422)
.send(req.t('Polja naslov, vprašanje in mnenje so obvezna!'))
}
const entry = await ConsultancyEntry.fetchById(id)
@@ -318,4 +401,157 @@ consultancy.deleteQuestion = async (req, res) => {
res.send()
}
async function consultancyRequestItems(
req,
res,
type,
url,
isAdminPage = false,
privilegeToSeAll = true, // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
page
) {
const searchString = req.query.q?.trim() ?? ''
/// //////////////////////////////////////////
// let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
// const pdList = intoDbArray(req.query.pd, 'always')
// allPrimaryDomains = allPrimaryDomains.map(entry => {
// if (pdList.includes(`${entry.id}`)) {
// entry.selected = true
// }
// return entry
// })
/// //////////////////////////////////////////
let assignedConsultant
if (
!privilegeToSeAll &&
req.user &&
!(req.user.hasRole('portal admin') || req.user.hasRole('consultancy admin'))
) {
assignedConsultant = req.user.id
} else {
assignedConsultant = undefined
}
const filters = {
status: type,
assignedConsultant,
primaryDomain: req.query.pd
}
if (!isAdminPage) {
filters.assignedConsultant = undefined
filters.status = 'published' // guard
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
// const page = +req.query.p > 0 ? +req.query.p : 1
const hitsQuery = generateQuery.consultancy(
searchString,
filters,
hitsPerPage,
page
)
const hits = await searchConsultancyEntryIndex(hitsQuery)
const numberOfAllHits = hits.body.hits.total.value
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
entry.lastName = entry.assignedConsultants[0]?.lastName
}
if (entry.assignedConsultants && entry.assignedConsultants.length > 1) {
entry.sharedAuthors = []
for (let i = 1; i < entry.assignedConsultants.length; i++) {
// skip first element (moderator)
entry.sharedAuthors.push(
`${entry.assignedConsultants[i].firstName} ${entry.assignedConsultants[i].lastName}`
)
}
}
if (entry.timeCreated) {
// TODO i18n date format
const date = new Date(entry.timeCreated)
entry.formattedTimeCreated = `${date.getDate()}. ${
date.getMonth() + 1
}. ${date.getFullYear()}`
}
return entry
})
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
if (!isAdminPage) {
entries.map(entry => {
const MAX_CHARACTER_LENGTH = 200
let appendAnswer = ''
let appendQuestion = ''
if (entry.answer && entry.answer.length > MAX_CHARACTER_LENGTH) {
appendAnswer = '...'
}
if (entry.question && entry.question.length > MAX_CHARACTER_LENGTH) {
appendQuestion = '...'
}
entry.answerSummary = `${entry.answer.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendAnswer}`
entry.question = `${entry.question.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendQuestion}`
return entry
})
}
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render(url, {
entries, // entryList,
userList,
numberOfAllPages,
isAdminPage,
section: consultancyAdminPageMapper(type),
queryCount: numberOfAllHits
})
}
// mapping required due to the inconsistent naming convention
function consultancyAdminPageMapper(type) {
if (type === 'in progress') {
return { inProgress: true }
}
if (type === 'review') {
return { prepared: true }
}
return { type: true }
}
module.exports = consultancy
+141 -8
View File
@@ -1,5 +1,7 @@
const { rm } = require('fs/promises')
const Dictionary = require('../../../models/dictionary')
const Entry = require('../../../models/entry')
const Extraction = require('../../../models/extraction')
const {
searchEntryIndex,
deleteEntryFromIndex,
@@ -9,6 +11,7 @@ const genEditorAllQuery = require('../../../models/helpers/search/generate-query
const { prepareEditorEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
const { getExportFilesPath } = require('../../../models/helpers/dictionary')
const dictionary = {}
@@ -115,6 +118,8 @@ dictionary.delete = async (req, res) => {
const dictionaryId = +req.params.dictionaryId
await Dictionary.delete(dictionaryId)
await deleteDictionaryEntriesFromIndex(dictionaryId)
const exportFilesPath = getExportFilesPath(dictionaryId)
await rm(exportFilesPath, { recursive: true, force: true })
res.end()
}
@@ -167,6 +172,56 @@ dictionary.listDomainLabels = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.listFilteredDomainLabels = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
let { q } = req.query
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredPaginationDomainLabels(
dictionaryId,
q,
resultsPerPage,
page
)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/domainLabelLister', {
dictionary: { id: dictionaryId },
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomainData = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
let { q } = req.query
const page = +req.query.p > 0 ? +req.query.p : 1
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredSecondaryDomains(q, resultsPerPage, page)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/secondaryDomainLister', {
page,
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomains = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
@@ -176,14 +231,92 @@ dictionary.listSecondaryDomains = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.extractionImport = async (req, res) => {
// TODO Import logic (Luka's task)
// const { id: dictionaryId, extractionId } = req.params
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ dictionaryId, extractionId, fromIndex, toIndex })
res.send('IMPORTING')
dictionary.showImportFromFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllImports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.showExportToFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchExports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.importFromExtraction = async (req, res) => {
const { id: dictionaryId, extractionId } = req.params
const { from, to } = req.body
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToImport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
await Dictionary.importFromExtraction(
dictionaryId,
req.user.id,
termCandidatesToImport
)
await Dictionary.indexIntoSearchEngine(dictionaryId)
res.end()
}
dictionary.exportBegin = async (req, res) => {
const dictionaryId = req.params.id
const exportParams = {
isValidFilter:
req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter,
isPublishedFilter:
req.body.isPublishedFilter === 'on'
? undefined
: req.body.isPublishedFilter,
statusFilter:
req.body.statusFilter === 'complete'
? 'complete'
: req.body.statusFilter === 'inEdit'
? 'in_edit'
: undefined,
isTerminologyReviewedFilter:
req.body.isTerminologyReviewedFilter === 'on'
? undefined
: req.body.isTerminologyReviewedFilter,
isLanguageReviewedFilter:
req.body.isLanguageReviewedFilter === 'on'
? undefined
: req.body.isLanguageReviewedFilter,
exportFileFormat: req.body.exportFileFormat
}
const exportId = await Dictionary.beginExport(dictionaryId, exportParams)
res.end()
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
// TODO Consider delegating processing of export to a seperate process or at least a seperate thread (much like importing, extraction, ...).
await Dictionary.processExport(exportId)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
module.exports = dictionary
+102 -52
View File
@@ -1,22 +1,27 @@
const { unlink, rm, mkdir } = require('fs/promises')
const { unlink, rm, mkdir, writeFile } = require('fs/promises')
const { promisify } = require('util')
const { URLSearchParams } = require('url')
const multer = require('multer')
const i18next = require('i18next')
const validator = require('validator')
const axios = require('axios')
const {
getExtractionFilesPath,
getDocumentsPath,
getStopTermsPath,
getConllusPath
getConllusPath,
getFileStats
} = require('../../../models/helpers/extraction')
const { checkIfcanBegin } = require('../../helpers/extraction')
const Extraction = require('../../../models/extraction')
const Domain = require('../../../models/domain')
const email = require('../../../models/email')
const { intoDbArray } = require('../../../models/helpers')
const { origin } = require('../../../config/keys')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { origin, extractionApiOrigin } = require('../../../config/keys')
const {
DEFAULT_HITS_PER_PAGE,
TEMP_EXPORT_PATH
} = require('../../../config/settings')
const MAX_FILE_NAME_LENGTH = 100
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
@@ -77,16 +82,19 @@ extraction.docsList = async (req, res) => {
extraction.docsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.docDelete = async (req, res) => {
@@ -108,16 +116,19 @@ extraction.stopTermsList = async (req, res) => {
extraction.stopTermsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.stopTermDelete = async (req, res) => {
@@ -141,7 +152,7 @@ extraction.ossSearch = [
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
})
const searchApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/steviloBesedilPoIskanju?${searchParams}`
const searchApiUrl = `${extractionApiOrigin}/oss/steviloBesedilPoIskanju?${searchParams}`
const { data: documentCount } = await axios.get(searchApiUrl)
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
@@ -183,28 +194,39 @@ extraction.begin = async (req, res) => {
res.send(timeStarted)
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const authorEmail = await Extraction.fetchAuthorEmail(extractionId)
const emailHtml = await renderAsync('email/extraction-done', {
extractionName,
extractionLink
})
await email.send({
to: authorEmail,
subject: 'Luščenje končano',
html: emailHtml
})
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const { email: authorEmail, language: authorLanguage } =
await Extraction.fetchAuthorData(extractionId)
const emailHtml = await renderAsync(
`email/extraction-done_${authorLanguage}`,
{
extractionName,
extractionLink
}
)
await email.send({
to: authorEmail,
subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
html: emailHtml
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
extraction.duplicate = async (req, res) => {
@@ -214,13 +236,36 @@ extraction.duplicate = async (req, res) => {
}
extraction.termCandidatesExport = async (req, res) => {
// TODO CSV logic (Luka's task)
// const extractionId = req.params.id
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ extractionId, fromIndex, toIndex })
res.download('public/images/help-amebis-logo-pug-demo.png')
// TODO More formats (CSV, TSV, TXT, ...)
const extractionId = req.params.id
const { from, to } = req.query
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToExport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
const exportFileName = `term_candidates_${extractionId}`
const exportFilePath = `${TEMP_EXPORT_PATH}/${exportFileName}`
await writeFile(
exportFilePath,
JSON.stringify({ terminoloski_kandidati: termCandidatesToExport })
)
const downloadAsync = promisify(res.download.bind(res))
try {
await downloadAsync(exportFilePath, 'term_candidates.json')
} finally {
await unlink(exportFilePath)
}
}
extraction.listFinishedForUser = async (req, res) => {
@@ -251,8 +296,11 @@ function extractionFileFilter(req, file, cb) {
fileType = 'stopTerms'
break
default:
return cb(Error('Invalid API endpoint'))
default: {
const customError = Error('Invalid API endpoint')
customError.displayInProd = true
return cb(customError)
}
}
const filenamePartsArray = file.originalname.split('.')
@@ -264,30 +312,32 @@ function extractionFileFilter(req, file, cb) {
(fileType === 'stopTerms' &&
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
) {
return cb(Error('Invalid file type'))
const customError = Error('Invalid file type')
customError.displayInProd = true
return cb(customError)
}
const fileName = filenamePartsArray.join('.')
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
return cb(
Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
const customError = Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
return cb(
Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
const customError = Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
return cb(
Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
const customError = Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
customError.displayInProd = true
return cb(customError)
}
req.fileType = fileType
+21
View File
@@ -66,6 +66,27 @@ exports.listMainEntries = async (req, res) => {
{}
)
// duplicated code below for filtering foreignent, optimize later
const categoriesLabels = Object.keys(entriesByCategory)
for (
let categoryIndex = 0;
categoryIndex < categoriesLabels.length;
categoryIndex++
) {
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
categoriesLabels[categoryIndex]
].map(entry => {
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
if (filters.targetLanguages.length > 0) {
return filters.targetLanguages.includes(`${foreignEntry.lang.id}`)
} else {
return true
}
})
return entry
})
}
// res.send({ page, numberOfAllPages, entries })
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)