const db = require('./db') const { searchEngineClient, ENTRY_INDEX } = require('./search-engine') const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers') const { prepareEntryForIndexing } = 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, 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'), definition: row.definition || null, synonyms: intoDbArray(row.synonym, 'undefined') }) } return agg }, []) const isValid = !!entry.term && (!!entry.definition || foreignLanguageContent.some(el => el.terms)) const values = [ dictionaryId, isValid, entry.status, entry.term || null, userId, entry.homonymSort || null, entry.wordforms || null, entry.accent || null, entry.pronunciation, intoDbArray(entry.domainLabels, 'always'), entry.label || null, entry.definition || null, intoDbArray(entry.synonyms), links, entry.other || null, foreignLanguageContent, intoDbArray(entry.image), intoDbArray(entry.audio), intoDbArray(entry.video) ] const text = `SELECT entry_new (${db.genParamStr(values)})` const { rows: [{ entry_new: entryId }] } = await db.query(text, values) return entryId } // // Fetch all entry terms of a single dictionary from DB. // Entry.fetchAll = async dictionaryId => { // const text = ` // SELECT // e.id, // e.is_valid as valid, // e.is_published as published, // e.term as term, // MAX(ef.term) as fterm, // CASE // WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 week' THEN 'T' // WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 month' THEN 'M' // WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 year' THEN 'L' // ELSE '' // END comment_age // FROM entry e // LEFT JOIN entry_foreign ef ON e.id = ef.entry_id // WHERE dictionary_id = $1 // GROUP BY id, is_valid, is_published, e.term, comment_age // ORDER BY e.term` // const value = [dictionaryId] // const { rows: fetchedTerms } = await db.query(text, value) // return fetchedTerms // } // Metoda za poizvedbo demo podatkov za določeno stran. // Entry.fetchPaginated = async (resultsPerPage, page) => { // const { // rows: [{ result }] // } = await db.query( // ` // SELECT jsonb_build_object( // 'pages_total', ( // SELECT CEIL(COUNT(*) / $1::float) // FROM demo_paginacija // ), // 'results', ARRAY( // SELECT jsonb_build_object( // dictionary_id, // term, // is_published, // is_terminology_reviewed, // is_language_reviewed, // status, // label, // definition, // synonym, // other, // image, // audio, // video // ) // FROM entry // LIMIT $1 // OFFSET $2 // ) // ) result // `, // [resultsPerPage, resultsPerPage * (page - 1)] // ) // return result // } // 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, 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'), definition: row.definition || null, synonyms: intoDbArray(row.synonym, 'undefined') }) } 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, entry.term || null, userId, entry.homonymSort || null, intoDbArray(entry.domainLabels, 'always'), entry.label || null, entry.definition || null, intoDbArray(entry.synonyms), links, entry.other || null, foreignLanguageContent, intoDbArray(entry.image), intoDbArray(entry.audio), intoDbArray(entry.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