Files
term_portal/express/models/entry.js
T

618 lines
18 KiB
JavaScript

const db = require('./db')
const { searchEngineClient, ENTRY_INDEX } = require('./search-engine')
const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers')
const {
prepareEntryForIndexing,
sanitizeField
} = require('./helpers/dictionary')
const Entry = {}
// Create a new dictionary entry in DB.
Entry.create = async (userId, dictionaryId, entry) => {
const pickedLinks = intoDbArray(entry.links, 'always')
const pickedType = intoDbArray(entry.type, 'always')
const links = pickedLinks.map((link, index) => ({
link: sanitizeField.toMixedBasic(link),
type: pickedType[index]
}))
const foreign = intoDbArray(entry.foreign, 'always')
const foreignLanguageContent = foreign.reduce((agg, row) => {
if (row.term || row.definition || row.synonym) {
agg.push({
language: row.code,
terms: intoDbArray(row.term, 'undefined')?.map(term =>
sanitizeField.toMixedBasic(term)
),
definition: sanitizeField.toMixedExtended(row.definition) || null,
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
)
})
}
return agg
}, [])
const isValid =
!!entry.term &&
(!!entry.definition || foreignLanguageContent.some(el => el.terms))
const values = [
dictionaryId,
isValid,
entry.status,
sanitizeField.toMixedBasic(entry.term) || null,
userId,
entry.homonymSort || null,
entry.wordforms || null,
entry.accent || null,
entry.pronunciation || null,
intoDbArray(entry.domainLabels, 'always').map(label =>
sanitizeField.toText(label)
),
sanitizeField.toMixedExtended(entry.label) || null,
sanitizeField.toMixedExtended(entry.definition) || null,
intoDbArray(entry.synonyms)?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
),
links,
sanitizeField.toMixedOther(entry.other) || null,
foreignLanguageContent,
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
]
const text = `SELECT entry_new (${db.genParamStr(values)})`
const {
rows: [{ entry_new: entryId }]
} = await db.query(text, values)
return entryId
}
// Fetch all data, related to single entry from DB.
Entry.fetchFull = async entryId => {
const text = `
SELECT
jsonb_strip_nulls(
jsonb_build_object(
'dictionary_id', e.dictionary_id,
'is_valid', e.is_valid,
'is_published', e.is_published,
'is_terminology_reviewed', e.is_terminology_reviewed,
'is_language_reviewed', e.is_language_reviewed,
'status', e.status,
'term', e.term,
'version', e.version,
'version_author', (
SELECT username
FROM "user" u
LEFT JOIN entry e ON e.version_author = u.id
WHERE e.id = $1
),
'homonym_sort', e.homonym_sort,
'label', e.label,
'definition', e.definition,
'synonyms', e.synonym,
'other', e.other,
'image', e.image,
'audio', e.audio,
'video', e.video,
'time_modified', e.time_modified,
'domain_labels', ARRAY(
SELECT name
FROM entry_domain_label edl
LEFT JOIN domain_label dl ON dl.id = edl.domain_label_id
WHERE entry_id = e.id
),
'links', ARRAY(
SELECT jsonb_build_object(
'type', type,
'link', link
)
FROM entry_link
WHERE entry_id = e.id
),
'foreign_entries', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'language_id', language_id,
'term', term,
'definition', definition,
'synonym', synonym
)
)
FROM entry_foreign
WHERE entry_id = e.id
),
'versions', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'version', version,
'version_time', version_time,
'version_author', (
SELECT username
FROM "user"
WHERE id = (version_snapshot['version_author'])::int
)
)
)
FROM entry_version_history
WHERE entry_id = e.id
)
)
) entry
FROM entry e
WHERE e.id = $1`
const value = [entryId]
const {
rows: [{ entry }]
} = await db.query(text, value)
return entry
}
// Fetch all data, related to single entry from DB with ordered foreign languages.
Entry.fetchFullWithOrderedForeignLanguages = async entryId => {
const text = `
SELECT
jsonb_strip_nulls(
jsonb_build_object(
'dictionary_id', e.dictionary_id,
'is_valid', e.is_valid,
'is_published', e.is_published,
'is_terminology_reviewed', e.is_terminology_reviewed,
'is_language_reviewed', e.is_language_reviewed,
'status', e.status,
'term', e.term,
'synonym', e.synonym,
'version', e.version,
'version_author', (
SELECT username
FROM "user" u
LEFT JOIN entry e ON e.version_author = u.id
WHERE e.id = $1
),
'homonym_sort', e.homonym_sort,
'label', e.label,
'definition', e.definition,
'synonyms', e.synonym,
'other', e.other,
'image', e.image,
'audio', e.audio,
'video', e.video,
'time_modified', e.time_modified,
'external_url', e.external_url,
'domain_labels', ARRAY(
SELECT name
FROM entry_domain_label edl
LEFT JOIN domain_label dl ON dl.id = edl.domain_label_id
WHERE entry_id = e.id
),
'links', ARRAY(
SELECT jsonb_build_object(
'type', type,
'link', link
)
FROM entry_link
WHERE entry_id = e.id
),
'foreign_entries', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'language_id', ef.language_id,
'term', ef.term,
'definition', ef.definition,
'synonym', ef.synonym
)
)
FROM entry e
LEFT JOIN entry_foreign ef on ef.entry_id = e.id
LEFT JOIN dictionary_language dl on dl.dictionary_id = e.dictionary_id AND ef.language_id = dl.language_id
LEFT JOIN language l on l.id = ef.language_id
WHERE entry_id = $1
ORDER BY dl.selection_order
),
'versions', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'version', version,
'version_time', version_time
)
)
FROM entry_version_history
WHERE entry_id = e.id
)
)
) entry
FROM entry e
WHERE e.id = $1`
const value = [entryId]
const {
rows: [{ entry }]
} = await db.query(text, value)
return entry
}
// Fetch single entry data from DB.
Entry.fetch = async entryId => {
const text = `
SELECT
dictionary_id,
term,
is_published,
is_terminology_reviewed,
is_language_reviewed,
homonym_sort,
status,
label,
definition,
synonym,
other,
image,
audio,
video
FROM entry
WHERE id = $1`
const value = [entryId]
const { rows: fetchedEntryData } = await db.query(text, value)
return fetchedEntryData[0]
}
// Fetch domain labels associated with a single entry from DB.
Entry.fetchDomainLabels = async entryId => {
const text = `
SELECT
dl.id,
dl.dictionary_id,
dl.name
FROM entry e
INNER JOIN entry_domain_label edl ON e.id = edl.entry_id
INNER JOIN domain_label dl ON edl.domain_label_id = dl.id
WHERE e.id = $1`
const value = [entryId]
const { rows: fetchedDomainLabels } = await db.query(text, value)
return fetchedDomainLabels
}
// Fetch foreign content associated with a single entry from DB.
Entry.fetchForeign = async entryId => {
const text = `
SELECT term, language_id, definition, synonym
FROM entry_foreign
WHERE entry_id=$1`
const value = [entryId]
const { rows: fetchedEntries } = await db.query(text, value)
return fetchedEntries
}
// Redundant. Use getInstanceSetting function instead.
// Entry.fetchEditingPhase = async () => {
// const text = `
// SELECT
// name, value
// FROM
// instance_settings
// WHERE
// name = 'can_publish_entries_in_edit'
// `
// const { rows: fetchedPhase } = await db.query(text)
// const aggregatedSettings = aggregateSettings(fetchedPhase)
// const deserializedSettings = deserialize.dictSettings(aggregatedSettings)
// return deserializedSettings
// }
// Delete foreign content of a single entry from DB.
Entry.deleteForeign = async entryId => {
const text = 'DELETE FROM entry_foreign WHERE entry_id = $1'
const value = [entryId]
await db.query(text, value)
}
// Delete domain label associations with a single entry from DB.
Entry.deleteDomainLabels = async entryId => {
const text = 'DELETE FROM entry_domain_label WHERE entry_id = $1'
const value = [entryId]
await db.query(text, value)
}
// Delete links of a single entry from DB.
Entry.deleteLinks = async entryId => {
const text = 'DELETE FROM entry_link WHERE entry_id = $1'
const value = [entryId]
await db.query(text, value)
}
// Delete single entry from DB.
Entry.delete = async entryId => {
const text = 'DELETE FROM entry WHERE id = $1 RETURNING dictionary_id'
const value = [entryId]
const {
rows: [{ dictionary_id: dictionaryId }]
} = await db.query(text, value)
return dictionaryId
}
// Delete foreign content for selected dictionary from DB.
Entry.deleteAllForeign = async dictionaryId => {
const text = `
DELETE FROM entry_foreign
WHERE entry_id
IN (SELECT id
FROM entry
WHERE dictionary_id = $1)`
const value = [dictionaryId]
await db.query(text, value)
}
// Delete all domain label associations for selected dictionary from DB.
Entry.deleteAllDomainLabels = async dictionaryId => {
const text = `
DELETE FROM entry_domain_label
WHERE entry_id
IN (SELECT id
FROM entry
WHERE dictionary_id = $1)`
const value = [dictionaryId]
await db.query(text, value)
}
// Delete all links for selected dictionary.
Entry.deleteAllLinks = async dictionaryId => {
const text = `
DELETE FROM entry_link
WHERE entry_id
IN (SELECT id
FROM entry
WHERE dictionary_id = $1)`
const value = [dictionaryId]
await db.query(text, value)
}
// (Re)index specific entry into entry search index.
Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
// TODO If this method is ever used for linked portals/dictionaries,
// TODO rework the source object below (already done in Dictionary.indexIntoSearchEngine).
const values = [entryId]
const text = `
SELECT
jsonb_strip_nulls(
jsonb_build_object(
'id', e.id,
'is_valid', e.is_valid,
'is_published', e.is_published,
'is_terminology_reviewed', e.is_terminology_reviewed,
'is_language_reviewed', e.is_language_reviewed,
'status', e.status,
'term', e.term,
'homonym_sort', e.homonym_sort,
'label', e.label,
'definition', e.definition,
'synonyms', e.synonym,
'other', e.other,
'time_most_recent_comment', e.time_most_recent_comment,
'domain_labels', ARRAY(
SELECT name
FROM entry_domain_label edl
LEFT JOIN domain_label dl ON dl.id = edl.domain_label_id
WHERE entry_id = e.id
),
'links', ARRAY(
SELECT link
FROM entry_link
WHERE entry_id = e.id
),
'foreign_entries', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'lang', jsonb_build_object(
'id', l.id,
'code', l.code,
'nameSl', l.name_sl,
'nameEn', l.name_en
),
'terms', ef.term,
'definition', ef.definition,
'synonyms', ef.synonym
)
)
FROM entry_foreign ef
LEFT JOIN language l ON l.id = ef.language_id
WHERE entry_id = e.id
)
)
) entry,
jsonb_strip_nulls(
jsonb_build_object(
'id', d.id,
'nameSl', d.name_sl,
'nameSlShort', d.name_sl_short,
'nameEn', d.name_en,
'status', d.status
)
) "dictionary",
jsonb_build_object(
'id', dp.id,
'nameSl', dp.name_sl,
'nameEn', dp.name_en
) primary_domain
FROM entry e
JOIN dictionary d ON d.id = e.dictionary_id
JOIN domain_primary dp ON dp.id = d.domain_primary_id
WHERE e.id = $1`
const {
rows: [dataToIndex]
} = await db.query(text, values)
const { dictionary, primary_domain: primaryDomain } = dataToIndex
let { entry } = dataToIndex
// TODO i18n Luka: index portal name for both languages?
const source = {
code: await getInstanceSetting('portal_code'),
name: await getInstanceSetting('portal_name_sl')
}
entry = prepareEntryForIndexing(entry)
entry.primaryDomain = primaryDomain
entry.dictionary = dictionary
entry.source = source
entry = removeHtmlTags(JSON.stringify(entry))
await searchEngineClient.index({
id: entryId,
index: ENTRY_INDEX,
body: entry,
refresh: shouldWait ? 'wait_for' : false
})
}
// Delete all entries for selected dictionary from search index.
Entry.deleteAllFromIndex = async dictionaryId => {
await searchEngineClient.deleteByQuery({
index: ENTRY_INDEX,
body: { query: { match: { 'dictionary.id': dictionaryId } } }
})
}
// Delete all entries for selected dictionary from DB.
Entry.deleteAll = async dictionaryId => {
const text = 'DELETE FROM entry WHERE dictionary_id = $1'
const value = [dictionaryId]
await db.query(text, value)
}
// Publish all entries for selected dictionary from DB that match the criteria.
Entry.publishAllQualified = async dictionaryId => {
const text = `
UPDATE entry
SET is_published = TRUE
WHERE
dictionary_id = $1
AND is_valid = TRUE
AND status = ANY(
CASE (SELECT value FROM instance_settings WHERE name = 'can_publish_entries_in_edit')
WHEN 'T' THEN ARRAY ['complete', 'in_edit']::entry_status[]
ELSE ARRAY ['complete']::entry_status[]
END
)
`
const value = [dictionaryId]
await db.query(text, value)
}
// Update single entry in DB.
Entry.update = async (userId, entry) => {
// Copied from Entry.create and modified.
const pickedLinks = intoDbArray(entry.links, 'always')
const pickedType = intoDbArray(entry.type, 'always')
const links = pickedLinks.map((link, index) => ({
link: sanitizeField.toMixedBasic(link),
type: pickedType[index]
}))
const foreign = intoDbArray(entry.foreign, 'always')
const foreignLanguageContent = foreign.reduce((agg, row) => {
if (row.term || row.definition || row.synonym) {
agg.push({
language: row.code,
terms: intoDbArray(row.term, 'undefined')?.map(term =>
sanitizeField.toMixedBasic(term)
),
definition: sanitizeField.toMixedExtended(row.definition) || null,
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
)
})
}
return agg
}, [])
const isValid =
!!entry.term &&
(!!entry.definition || foreignLanguageContent.some(el => el.terms))
const values = [
entry.entryId,
isValid,
!!entry.isPublished,
!!entry.isTerminologyReviewed,
!!entry.isLanguageReviewed,
entry.status,
sanitizeField.toMixedBasic(entry.term) || null,
userId,
entry.homonymSort || null,
intoDbArray(entry.domainLabels, 'always').map(label =>
sanitizeField.toText(label)
),
sanitizeField.toMixedExtended(entry.label) || null,
sanitizeField.toMixedExtended(entry.definition) || null,
intoDbArray(entry.synonyms)?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
),
links,
sanitizeField.toMixedOther(entry.other) || null,
foreignLanguageContent,
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
]
const text = `SELECT entry_update (${db.genParamStr(values)})`
const {
rows: [{ entry_update: dictionaryId }]
} = await db.query(text, values)
return dictionaryId
}
// // Fetch all versions (with timestamps) of a single entry from DB.
// Entry.fetchVersions = async entryId => {
// const { rows: historyVersions } = await db.query(
// 'SELECT version, version_time FROM entry_version_history WHERE entry_id = $1',
// [entryId]
// )
// return historyVersions
// }
// Fetch a single version snapshot of a single entry from DB.
Entry.fetchVersionSnapshot = async (entryId, version) => {
const {
rows: [{ version_snapshot: historySnapshot, author }]
} = await db.query(
`SELECT v.version_snapshot, (
SELECT u.username
FROM "user" u
WHERE u.id = (v.version_snapshot['version_author'])::int) as author
FROM entry_version_history v WHERE v.entry_id = $1 and v.version = $2`,
[entryId, version]
)
return { data: historySnapshot, author }
}
/* Fetch by language and entry Id. Note that this version includes the language name */
Entry.fetchForeignEntryById = async entryId => {
const text = `
SELECT
entry_id,
language_id,
code,
name_sl,
name_en,
term,
definition,
synonym
FROM entry_foreign ef
INNER JOIN language lang ON ef.language_id = lang.id
WHERE ef.entry_id = $1`
const value = [entryId]
const { rows: fetchedDomainLabels } = await db.query(text, value)
return fetchedDomainLabels
}
module.exports = Entry