Initial commit

This commit is contained in:
Luka Romih
2022-12-07 04:43:36 +01:00
commit 257f3c354f
496 changed files with 55373 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
const debug = require('debug')('app:batch')
// const db = require('../models/db')
function update(dbc, report, status, done) {
const reps = JSON.stringify(report)
debug('update', status, reps.length)
if (status === 'D' || status === 'F') {
dbc.query(
'UPDATE batch SET report=$1, job_state=$2, ended=now(), changed=now() WHERE id=$3',
[reps, status, report.id],
err => {
done(err)
}
)
} else {
dbc.query(
'UPDATE batch SET report=$1, changed=now() WHERE id=$2',
[reps, report.id],
err => {
done(err)
}
)
}
}
module.exports = {
Report: function (jobName, threadId) {
this.id = 0
this.name = jobName
this.thread = threadId
this.warnings = 0
this.progress = {
total_percent: 0,
phase_name: '',
phase_percent: 0,
item_name: ''
}
this.trace = [] // here we push messages and errors
},
/**
* initializes batch tracker by adding a record to batch table
* @param job_name name of the job
* @param done (err, BatchId)
*/
init: function (dbc, report, done) {
debug('initializing', report)
dbc.query(
'INSERT INTO batch (job_name, job_state, thread, report) VALUES($1,$2,$3,$4) RETURNING id',
[report.name, 'R', report.thread, JSON.stringify(report)],
(err, result) => {
if (err) return done(err)
debug(result)
report.id = result.rows[0].id
done()
}
)
},
/**
* Close the batch by marking its status as completed and setting end_time.
* @param report object containing full batch report
* @param msg final message
* @param done
*/
finalize: function (dbc, report, msg, done) {
debug('finalizing', msg)
if (msg) report.trace.push(msg)
else report.trace.push('done')
update(dbc, report, 'D', done)
},
/**
* Report on phase. If phase name changes add new phase message to trace.
* @param report object containing full batch report
* @param phase name of phase
* @param percentJob percent (total for job)
* @param done
*/
reportPhase: function (dbc, report, phase, percentJob, done) {
debug('phase', phase, percentJob)
if (report.progress.phase_name !== phase) {
report.trace.push('starting phase ' + phase)
report.progress.phase_percent = 0
report.progress.item_name = ''
}
report.progress.phase_name = phase
report.progress.total_percent = percentJob
update(dbc, report, '', done)
},
/**
* Report on item processed. This is sublevel of phase
* @param report object containing full batch report
* @param item name of item
* @param percentPhase percent of current phase done
* @param done
*/
reportItem: function (dbc, report, item, percentPhase, done) {
debug('item', item, percentPhase)
report.progress.item_name = item
report.progress.phase_percent = percentPhase
update(dbc, report, '', done)
},
/**
* Report error during process and close batch with status Failed and setting end_time.
* @param report object containing full batch report
* @param err the error. If it contains stack it will be included in trace
* @param done
*/
fail: function (dbc, report, err, done) {
debug('fail', err)
if (err.stack) report.trace.push(err.stack)
else if (err.message) report.trace.push(err.message)
else report.trace.push(JSON.stringify(err))
update(dbc, report, 'F', done)
}
}
+18
View File
@@ -0,0 +1,18 @@
const Redis = require('ioredis')
const debug = require('debug')('termPortal:models/cache')
const client = new Redis({ host: 'redis' })
client.on('error', debug)
const isReadyPromise = new Promise(resolve => client.on('ready', resolve))
client.waitForConnection = async () => {
// eslint-disable-next-line no-console
console.log('Waiting for cache to be ready')
await isReadyPromise
// eslint-disable-next-line no-console
console.log('Cache is ready')
}
module.exports = client
+432
View File
@@ -0,0 +1,432 @@
const db = require('./db')
const debug = require('debug')('termPortal:models/comment')
const User = require('../models/user')
class Comment {
// Deserialize flat data into an organized comment object.
constructor({
id,
message,
author_first_name: authorFirstName,
author_last_name: authorLastName,
time_created: timeCreated,
status,
quote_message: quoteMessage,
quote_author_first_name: quoteAuthorFirstName,
quote_author_last_name: quoteAuthorLastName,
quote_time_created: quoteTimeCreated
}) {
this.id = id
this.message = message
this.author = { firstName: authorFirstName, lastName: authorLastName }
this.timeCreated = timeCreated
this.status = status
this.quote = quoteMessage
? {
message: quoteMessage,
author: {
firstName: quoteAuthorFirstName,
lastName: quoteAuthorLastName
},
timeCreated: quoteTimeCreated
}
: null
}
// Fetch comments from DB.
static async list(filters, user, resultsPerPage, page) {
const { ctxType, ctxId } = filters
if (!ctxType) throw Error('Missing context type')
const values = [resultsPerPage, ctxType]
let whereClause = 'WHERE c.context_type = $2'
if (ctxId) {
whereClause += ' AND c.context_id = $3'
values.push(ctxId)
}
const nonModeratedContexts = ['entry_dict_int', 'entry_consult_int']
// In internal/non-moderated contexts, fetch all context specific comments and never show visibility toggles.
// In external/moderated contexts, for users with elevated context related rights, show all comments and show visibility toggles,
// for everyone else, show only visible comments and don't show visibility toggles.
let displayVisibilityToggles = false
if (!nonModeratedContexts.includes(ctxType)) {
let isCommentModerator = false
if (user) {
switch (ctxType) {
case 'portal':
if (user.hasRole('portal admin')) isCommentModerator = true
break
case 'dictionary':
if (
user.hasRole('portal admin') ||
user.hasRole('dictionaries admin')
) {
isCommentModerator = true
}
break
case 'consultancy':
if (
user.hasRole('portal admin') ||
user.hasRole('consultancy admin')
) {
isCommentModerator = true
}
break
case 'entry_dict_ext':
if (
user.hasRole('portal admin') ||
user.hasRole('consultancy admin') ||
user.hasDictionaryRole(ctxId, 'administration')
) {
isCommentModerator = true
}
break
default:
throw Error('Invalid context type')
}
}
if (isCommentModerator) {
displayVisibilityToggles = true
} else {
whereClause += " AND c.status = 'visible'"
}
}
let offsetValue
if (page === 'last') {
offsetValue = `(SELECT (CEIL(COUNT(*) / $1::float) - 1) * $1 FROM comment c ${whereClause})`
} else {
offsetValue = resultsPerPage * (page - 1)
}
const text = `
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $1::float)
FROM comment c
${whereClause}
),
'comment_count', (
SELECT COUNT(*)
FROM comment c
${whereClause}
),
'comments', ARRAY(
SELECT jsonb_build_object(
'id', c.id,
'message', c.message,
'timeCreated', c.time_created,
'status', c.status,
'author', (
SELECT jsonb_build_object(
'firstName', cu.first_name,
'lastName', cu.last_name
)
),
'showEye', ${displayVisibilityToggles},
'quote', (
SELECT
CASE
WHEN c.quoted_comment_id IS NULL THEN NULL
ELSE jsonb_build_object(
'message', q.message,
'timeCreated', q.time_created,
'author', (
SELECT jsonb_build_object(
'firstName', qu.first_name,
'lastName', qu.last_name
)
)
)
END
)
)
FROM comment c
LEFT JOIN "user" cu
ON cu.id = c.author_id
LEFT JOIN comment q
ON q.id = c.quoted_comment_id
LEFT JOIN "user" qu
ON qu.id = q.author_id
${whereClause}
ORDER BY c.time_created
LIMIT $1
OFFSET ${offsetValue}
)
) results`
const { rows } = await db.query(text, values)
const { results } = rows[0]
return results
}
// Insert a new comment into DB.
static async create(comment, userId) {
const text =
'INSERT INTO comment (message, author_id, context_type, context_id, quoted_comment_id) VALUES ($1, $2, $3, $4, $5)'
const values = [
comment.message,
userId,
comment.ctxType,
comment.ctxId,
comment.quoteId
]
await db.query(text, values)
}
static async updateStatus(status, id) {
const values = [id, status]
const text = `
UPDATE comment
SET status = $1
WHERE id = $2`
await db.query(text, values)
}
// Insert a new demo comment into DB.
static async createDemo(comment) {
const text =
"INSERT INTO comment (message, author_id, context_type, quoted_comment_id) VALUES ($1, $2, 'portal', $3) RETURNING id"
const values = [comment.message, pickRandomMockUserId(), comment.quoteId]
const { rows } = await db.query(text, values)
const idOfInsertedComment = rows[0].id
const text2 = `${selectAllCommentsQueryString} WHERE c.id = ${idOfInsertedComment}`
const { rows: rows2 } = await db.query(text2)
const insertedComment = rows2[0]
const deserializedComment = new this(insertedComment)
return deserializedComment
}
// Seed DB with <commentCount> random comments.
static async seed(commentCount) {
const seedTasks = []
for (let i = 0; i < commentCount; i++) {
seedTasks.push(this.createDemo({ message: pickRandomMockMessage() }))
}
const seededComments = await Promise.all(seedTasks)
debug(`Successfully seeded ${commentCount} comments`)
debug('Comments:')
seededComments.forEach(comment => debug(comment))
}
// Clear all comments from DB.
static async clear() {
await db.query('TRUNCATE comment')
}
}
// Base SQL query string to fetch all comments.
// Can be extended with a WHEN filter clause.
const selectAllCommentsQueryString = `SELECT
c.id,
c.message,
cu.first_name author_first_name,
cu.last_name author_last_name,
c.time_created,
c.status,
q.message quote_message,
qu.first_name quote_author_first_name,
qu.last_name quote_author_last_name,
q.time_created quote_time_created
FROM comment c
LEFT JOIN "user" cu
ON cu.id = c.author_id
LEFT JOIN comment q
ON q.id = c.quoted_comment_id
LEFT JOIN "user" qu
ON qu.id = q.author_id`
// A list of messages of varying length for DB seeding.
const mockMessageVariations = [
'Lorem ipsum dolor sit amet.',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem obcaecati ut reprehenderit explicabo, adipisci atque! Repudiandae eos facilis veniam modi.',
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab dicta error architecto id soluta laborum pariatur saepe doloribus voluptatem voluptas totam placeat, inventore rem! Tempore illum deleniti esse nemo. Amet.',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tristique at sem eu ultricies. Curabitur cursus efficitur ipsum, et iaculis ipsum egestas vel egestas vestibulum nec odio posuere, mollis diam et, bibendum velit. Proin non velit nec dui luctus dolor.',
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Eveniet deleniti ad quasi, ea, recusandae esse autem expedita tempora molestiae ipsa labore magnam dolorem nostrum, corrupti sint obcaecati. Voluptatum molestiae, qui laudantium voluptatibus eius, ratione voluptate, eaque quae alias dicta pariatur?'
]
// A list of users for DB seeding and random asigning to new
// comments until authentication and session mechanism are in place.
const mockUsers = [
{ firstName: 'Primož', lastName: 'Roglič' },
{ firstName: 'Tadej', lastName: 'Pogačar' },
{ firstName: 'Krištof', lastName: 'Kolumb' },
{ firstName: 'Rudolf', lastName: 'Maister' },
{ firstName: 'Ricky', lastName: 'Rickardo' },
{ firstName: 'Freddy', lastName: 'Mercury' },
{ firstName: 'Roger', lastName: 'Moore' },
{ firstName: 'Michael', lastName: 'Jackson' },
{ firstName: 'John', lastName: 'Elton' },
{ firstName: 'Harry', lastName: 'Potter' }
]
function pickRandomMockMessage() {
return mockMessageVariations[
Math.floor(Math.random() * mockMessageVariations.length)
]
}
function pickRandomMockUserId() {
return Math.ceil(Math.random() * mockUsers.length)
}
async function seedMockUsersInDb() {
const { rows } = await db.query('SELECT COUNT(*) user_count FROM "user"')
const userCount = rows[0].user_count
if (+userCount) return 'Users already exist'
await Promise.all(
mockUsers.map(async user => {
const text =
'INSERT INTO "user"(username, first_name, last_name, email, bcrypt_hash) VALUES ($1, $2, $3, $4, \'dummyBcryptHash\') RETURNING *'
const values = [
`${user.firstName}_${user.lastName}`,
user.firstName,
user.lastName,
`${user.firstName}.${user.lastName}@rsdo.com`
]
const { rows } = await db.query(text, values)
const createdUser = rows[0]
debug(`Created user: ${JSON.stringify(createdUser)}`)
})
)
return 'Successfully seeded all users'
}
async function seedPortalAdmin() {
const MOCK_ADMIN_BASE = 'admin'
const {
rows: [mockAdmin]
} = await db.query('SELECT id FROM "user" WHERE username = $1', [
MOCK_ADMIN_BASE
])
if (mockAdmin) {
return `Portal admin already exists (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
}
const adminUser = {
username: MOCK_ADMIN_BASE,
firstName: MOCK_ADMIN_BASE,
lastName: MOCK_ADMIN_BASE,
password: MOCK_ADMIN_BASE,
email: `${MOCK_ADMIN_BASE}@rsdo.com`
}
const userId = await User.create(adminUser)
const assignAdminRole = db.query(
`INSERT INTO user_role (user_id, role_name)
VALUES
($1, 'portal admin'),
($1, 'dictionaries admin'),
($1, 'consultancy admin'),
($1, 'consultant')`,
[userId]
)
const activateAdminUser = db.query(
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
[adminUser.username]
)
await Promise.all([assignAdminRole, activateAdminUser])
return `Successfully seeded portal admin (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
}
async function seedConsultants() {
const {
rows: [mockConsultancyAdmin]
} = await db.query('SELECT id FROM "user" WHERE username = $1', ['cadmin'])
if (mockConsultancyAdmin) {
return "Consultants already exist: 'cadmin', 'consultant1', 'consultant2', 'consultant3'"
}
const cadminUser = {
username: 'cadmin',
firstName: 'cadmin',
lastName: 'cadmin',
password: 'cadmin',
email: 'cadmin@rsdo.com'
}
const userId = await User.create(cadminUser)
const assignConsultancyAdminRole = db.query(
`INSERT INTO user_role (user_id, role_name)
VALUES
($1, 'consultancy admin')`,
[userId]
)
const activateConsultancyAdminUser = db.query(
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
[cadminUser.username]
)
await Promise.all([assignConsultancyAdminRole, activateConsultancyAdminUser])
for (let i = 1; i <= 3; i++) {
const consultant = {
username: `consultant${i}`,
firstName: `consultant${i}`,
lastName: `consultant${i}`,
password: `consultant${i}`,
email: `consultant${i}@rsdo.com`
}
const userId = await User.create(consultant)
const assignConsultantRole = db.query(
`INSERT INTO user_role (user_id, role_name)
VALUES
($1, 'consultant')`,
[userId]
)
const activateConsultant = db.query(
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
[consultant.username]
)
await Promise.all([assignConsultantRole, activateConsultant])
}
return `Successfully seeded consultants 'cadmin', 'consultant1', 'consultant2', 'consultant3'`
}
Comment.seedDummyData = () => {
// Seed DB with mock users on empty DB.
// seedMockUsersInDb()
// .then(debug)
// .catch(err => {
// debug('Users not seeded.')
// debug(err)
// })
// Seed DB with mock portal admin user on empty DB.
// TODO Replace with a more robust solution for production.
seedPortalAdmin()
.then(debug)
.catch(err => {
debug('Portal admin not seeded.')
debug(err)
})
// Seed DB with mock consultancy admin and consultant users on empty DB.
// seedConsultants()
// .then(debug)
// .catch(err => {
// debug('Consultants not seeded.')
// debug(err)
// })
}
module.exports = Comment
+754
View File
@@ -0,0 +1,754 @@
const db = require('./db')
const {
searchEngineClient,
deleteConsultancyEntriesFromIndex,
CONSULTANCY_ENTRY_INDEX
} = require('./search-engine')
const { removeHtmlTags } = require('./helpers')
class ConsultancyEntry {
constructor({
id,
id_external: idExternal,
time_created: timeCreated,
status,
author_id: authorId,
institution,
description,
domain_primary_id_initial: domainPrimaryIdInitial,
existing_solutions: existingSolutions,
examples_of_use: examplesOfUse,
time_published: timePublished,
title,
question,
answer,
path,
answer_authors: answerAuthors,
domain_primary_id: domainPrimaryId,
first_name: firstName,
last_name: lastName,
is_moderator: isModerator,
formatted_time_created: formattedTimeCreated,
formatted_time_published: formattedTimePublished
}) {
this.id = id
this.idExternal = idExternal
this.timeCreated = timeCreated
this.status = status
this.authorId = authorId
this.institution = institution
this.description = description
this.domainPrimaryIdInitial = domainPrimaryIdInitial
this.existingSolutions = existingSolutions
this.examplesOfUse = examplesOfUse
this.timePublished = timePublished
this.title = title
this.question = question
this.answer = answer
this.path = path
this.answerAuthors = answerAuthors
this.domainPrimaryId = domainPrimaryId
this.firstName = firstName
this.lastName = lastName
this.isModerator = isModerator
this.formattedTimeCreated = formattedTimeCreated
this.formattedTimePublished = formattedTimePublished
}
// Fetch all consultancy entries from DB.
static async fetchAll() {
// TODO Luka: Miha, define specific fields instead of using *.
const { rows: fetchedConsEntries } = await db.query(`
SELECT *
FROM consultancy_entry
ORDER BY time_created DESC`)
const deserializedConsEntries = fetchedConsEntries.map(
consEntry => new this(consEntry)
)
return deserializedConsEntries
}
// Fetch consultancy entry by ID
// to_char(time_created,'HH24:MI:SS DD/MM/YYYY')
static async fetchByIdWithFormattedTime(id) {
const { rows: fetchedConsEntry } = await db.query(
`
SELECT
id,
id_external,
to_char(time_created,'DD. MM. YYYY') time_created,
status,
author_id,
institution,
description,
domain_primary_id_initial,
existing_solutions,
examples_of_use,
to_char(time_published,'DD. MM. YYYY') time_published,
title,
question,
answer,
path,
answer_authors,
domain_primary_id
FROM consultancy_entry
WHERE id=$1`,
[id]
)
return new this(fetchedConsEntry[0])
}
static async fetchById(id) {
const { rows: fetchedConsEntry } = await db.query(
`
SELECT
id,
id_external,
to_char(time_created, 'DD. MM. YYYY') time_created,
status,
author_id,
institution,
description,
domain_primary_id_initial,
existing_solutions,
examples_of_use,
to_char(time_published,'DD. MM. YYYY') time_published,
title,
question,
answer,
path,
answer_authors,
domain_primary_id
FROM consultancy_entry
WHERE id=$1`,
[id]
)
return new this(fetchedConsEntry[0])
}
// Fetch dictionaries admin emails.
static async fetchConsultancyAdminEmails() {
const query = {
text: `
SELECT email FROM "user" WHERE id
IN(SELECT user_id FROM user_role
WHERE role_name = 'consultancy admin')`,
rowMode: 'array'
}
const { rows: users } = await db.query(query)
const emails = users.flat()
return emails
}
static async fetchModeratorEmail(entryId) {
const query = {
text: `
SELECT email FROM "user" WHERE id
IN (SELECT user_id FROM consultancy_entry_consultant cec
WHERE cec.entry_id=$1 AND cec.is_moderator=true)`,
rowMode: 'array'
}
const { rows: users } = await db.query(query, [entryId])
const emails = users.flat()
return emails
}
// Fetch all consultancy entries filtered by status from DB.
static async fetchAllByStatus(status) {
// TODO Luka: Miha, define specific fields instead of using *.
const sqlQuery = `
SELECT *, to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created
FROM consultancy_entry
WHERE status=$1
ORDER BY time_created DESC`
const values = [status]
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
const deserializedConsEntries = fetchedConsEntries.map(
consEntry => new this(consEntry)
)
return deserializedConsEntries
}
// Fetch all consultancy entries filtered by status from DB.
static async fetchAllByStatusCount(status) {
// TODO Luka: Miha, define specific fields instead of using *.
const sqlQuery = `
SELECT COUNT(id)
FROM consultancy_entry
WHERE status=$1`
const values = [status]
const { rows } = await db.query(sqlQuery, values)
return rows[0].count
}
static async fetchWithStatusByIdCount(status, id) {
const sqlQuery = `
SELECT COUNT(DISTINCT(ce.id))
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1 and cec.user_id=$2`
const values = [status, id]
const { rows } = await db.query(sqlQuery, values)
return rows[0].count
}
static async fetchWithStatusById(status, id) {
const sqlQuery = `
SELECT ce.id,
to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created,
author_id,
institution,
description,
domain_primary_id_initial,
to_char(time_published, 'FMDD. FMMM. YYYY') time_published,
title,
question,
answer,
path,
answer_authors,
domain_primary_id,
first_name,
last_name,
cec.is_moderator
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1 and cec.user_id=$2
ORDER BY time_created DESC`
/* filter them in pug by displaying moderator, and showing authors
of non moderator */
/* AND cec.is_moderator='true';` */
const values = [status, id]
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
const deserializedConsEntries = fetchedConsEntries.map(
consEntry => new this(consEntry)
)
return deserializedConsEntries
}
static async fetchInProgressById(id) {
return await this.fetchWithStatusById('in progress', id)
}
static async fetchPublishedById(id) {
return await this.fetchWithStatusById('published', id)
}
// Fetch entry count.
static async fetchAllByStatusWithAuthorDataCount(status) {
const sqlQuery = `
SELECT COUNT(DISTINCT(ce.id))
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1`
/* filter them in pug by displaying moderator, and showing authors
of non moderator */
/* AND cec.is_moderator='true';` */
const values = [status]
const { rows } = await db.query(sqlQuery, values)
await db.query(sqlQuery, values)
return rows[0].count
}
// Fetch all consultancy entries filtered by status from DB.
static async fetchAllByStatusWithAuthorData(status) {
const sqlQuery = `
SELECT ce.id,
to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created,
author_id,
institution,
description,
domain_primary_id_initial,
to_char(time_published, 'FMDD. FMMM. YYYY') time_published,
title,
question,
answer,
path,
answer_authors,
domain_primary_id,
first_name,
last_name,
cec.is_moderator
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1
ORDER BY time_created DESC`
/* filter them in pug by displaying moderator, and showing authors
of non moderator */
/* AND cec.is_moderator='true';` */
const values = [status]
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
const deserializedConsEntries = fetchedConsEntries.map(
consEntry => new this(consEntry)
)
return deserializedConsEntries
}
// Fetch all new consultancy entries from DB.
static async fetchAllNew() {
const newEntries = await this.fetchAllByStatus('new')
return newEntries
}
static async fetchAllRejected() {
const newEntries = await this.fetchAllByStatusWithAuthorData('rejected')
return newEntries
}
static async fetchAllInProgress() {
const newEntries = await this.fetchAllByStatusWithAuthorData('in progress')
return newEntries
}
static async fetchAllPrepared() {
const newEntries = await this.fetchAllByStatusWithAuthorData('review')
return newEntries
}
static async fetchAllPublished() {
const newEntries = await this.fetchAllByStatusWithAuthorData('published')
return newEntries
}
static async fetch5MostRecentPublished() {
const sqlQuery = `
SELECT ce.id,
to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created,
author_id,
institution,
description,
domain_primary_id_initial,
to_char(time_published, 'FMDD. FMMM. YYYY') time_published,
title,
question,
answer,
path,
answer_authors,
domain_primary_id,
first_name,
last_name,
cec.is_moderator
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1
ORDER BY time_published DESC
LIMIT 5`
/* filter them in pug by displaying moderator, and showing authors
of non moderator */
/* AND cec.is_moderator='true';` */
const values = ['published']
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
const deserializedConsEntries = fetchedConsEntries.map(
consEntry => new this(consEntry)
)
return deserializedConsEntries
}
static async fetchPublishedCount() {
const sqlQuery = `
SELECT COUNT(DISTINCT(ce.id))
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.status=$1`
/* filter them in pug by displaying moderator, and showing authors
of non moderator */
/* AND cec.is_moderator='true';` */
const values = ['published']
const { rows } = await db.query(sqlQuery, values)
return rows[0].count
}
/*
static async fetchAuthorsFirstAndLastName(entryId, userId) {
const sqlQuery = `
SELECT u.first_name u.last_name
FROM "user" u INNER JOIN "consultancy_entry_consultant" cec ON u.id = cec.user_id
WHERE cec.entry_id=$1 and cec.user_id=$2`
const values = [entryId, userId]
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
return fetchedConsEntries
}
*/
/* Get shared authors in progress,
answer_authors only get published when the question is published */
static async getSharedAuthorsArrayBeforePublish(entryId) {
const sqlQuery = `SELECT cec.entry_id, cec.user_id,
u.first_name, u.last_name, u.username
FROM "consultancy_entry_consultant" cec INNER JOIN
"user" u ON u.id = cec.user_id
WHERE cec.entry_id=$1 AND cec.is_moderator='false';`
const values = [entryId]
const { rows } = await db.query(sqlQuery, values)
return rows
}
static async getModerator(entryId) {
const sqlQuery = `
SELECT u.id, u.first_name, u.last_name
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.id=$1 AND cec.is_moderator=true;`
const values = [entryId]
const { rows } = await db.query(sqlQuery, values)
return rows[0]
}
static async getEditors(entryId) {
const sqlQuery = `
SELECT u.id, u.first_name, u.last_name
FROM "consultancy_entry" ce
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
INNER JOIN "user" u ON u.id = cec.user_id
WHERE ce.id=$1`
const values = [entryId]
const { rows } = await db.query(sqlQuery, values)
return rows
}
static async getSharedAuthorsArray(entryId) {
const sqlQuery = `SELECT answer_authors FROM "consultancy_entry"
WHERE id=$1;`
const values = [entryId]
const { rows } = await db.query(sqlQuery, values)
return rows[0]
}
static async updateSharedAuthorsArray(entryId, authors) {
const sqlQuery = `UPDATE "consultancy_entry" SET answer_authors=$2
WHERE id=$1;`
const values = [entryId, authors]
await db.query(sqlQuery, values)
}
static async createQuestion(consultancyEntry) {
const sqlQuery = `INSERT INTO consultancy_entry (
status,
author_id,
institution,
description,
domain_primary_id_initial,
existing_solutions,
examples_of_use
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
)
RETURNING id;`
const values = [
consultancyEntry.status,
consultancyEntry.authorId,
consultancyEntry.institution,
consultancyEntry.description,
consultancyEntry.domainPrimaryIdInitial || null,
consultancyEntry.existingSolutions,
consultancyEntry.examplesOfUse
]
const {
rows: [{ id }]
} = await db.query(sqlQuery, values)
return id
}
static async updateQuestion(consultancyEntry) {
await db.transaction(async dbClient => {
const sqlQuery = `UPDATE consultancy_entry SET
id_external=$2,
status=$3,
author_id=$4,
institution=$5,
description=$6,
domain_primary_id_initial=$7,
existing_solutions=$8,
examples_of_use=$9,
title=$10,
question=$11,
answer=$12,
path=$13,
answer_authors=$14,
domain_primary_id=$15
WHERE id=$1;`
const values = [
consultancyEntry.id,
consultancyEntry.idExternal,
consultancyEntry.status,
consultancyEntry.authorId,
consultancyEntry.institution,
consultancyEntry.description,
consultancyEntry.domainPrimaryIdInitial,
consultancyEntry.existingSolutions,
consultancyEntry.examplesOfUse,
consultancyEntry.title,
consultancyEntry.question,
consultancyEntry.answer,
consultancyEntry.path,
consultancyEntry.answerAuthors,
consultancyEntry.domainPrimaryId
]
await dbClient.query(sqlQuery, values)
await dbClient.query(
`UPDATE "consultancy_entry" SET status=$2
WHERE id=$1;`,
[consultancyEntry.id, 'in progress']
)
})
}
static async deleteQuestion(id) {
const sqlQuery = `DELETE FROM consultancy_entry
WHERE id=$1;`
const values = [id]
await db.query(sqlQuery, values)
}
/** refactor in case you need both moderator and non moderator
* insertions as a common function
*/
static async insertNonModerator(entryId, userId) {
const sqlQuery = `INSERT INTO "consultancy_entry_consultant" (entry_id, user_id, is_moderator) VALUES
($1, $2, 'false');
`
const values = [entryId, userId]
const { rows } = await db.query(sqlQuery, values)
return rows
}
static async assignWorkInProgress(entryId, userId) {
await db.transaction(async dbClient => {
let sqlQuery = `SELECT entry_id FROM consultancy_entry_consultant
WHERE entry_id=$1 AND is_moderator='true'`
let values = [entryId]
const { rows } = await dbClient.query(sqlQuery, values)
if (rows.length) {
await dbClient.query(
`DELETE FROM consultancy_entry_consultant
WHERE entry_id=$1 AND is_moderator='true'`,
values
)
}
sqlQuery = [
`UPDATE "consultancy_entry" SET status='in progress'
WHERE id=$1;`,
`INSERT INTO "consultancy_entry_consultant" (entry_id, user_id, is_moderator) VALUES
($1, $2, 'true');
`
]
values = [[entryId], [entryId, userId]]
for (let i = 0; i < sqlQuery.length; i++) {
await dbClient.query(sqlQuery[i], values[i])
}
})
}
static async updateConsultancyEntryStatus(entryId, status) {
const sqlQuery = `UPDATE "consultancy_entry" SET status=$2
WHERE id=$1;`
const values = [entryId, status]
await db.query(sqlQuery, values)
}
static async rejectEntry(entryId) {
await this.updateConsultancyEntryStatus(entryId, 'rejected')
}
static async sendToReview(entryId) {
await this.updateConsultancyEntryStatus(entryId, 'review')
}
static async publish(entryId, answerAuthors) {
await db.transaction(async dbClient => {
await dbClient.query(
`UPDATE "consultancy_entry" SET status=$2
WHERE id=$1;`,
[entryId, 'published']
)
if (answerAuthors) {
answerAuthors = answerAuthors.split(',').filter(author => author !== '')
await dbClient.query(
`UPDATE "consultancy_entry"
SET answer_authors=$2
WHERE id=$1;`,
[entryId, answerAuthors]
)
}
await dbClient.query(
`UPDATE "consultancy_entry"
SET time_published=CURRENT_TIMESTAMP
WHERE id=$1;`,
[entryId]
)
})
}
static async removeConsultantForEntry(entryId, userId) {
// TODO check for any other tables where entry id is foreign key
await db.query(
`DELETE FROM consultancy_entry_consultant
WHERE entry_id=$1 AND user_id=$2`,
[entryId, userId]
)
}
static async removeEntry(id) {
// TODO check for any other tables where entry id is foreign key
await db.transaction(async dbClient => {
await dbClient.query(
`DELETE FROM consultancy_entry_consultant
WHERE entry_id=$1`,
[id]
)
await dbClient.query(
`DELETE FROM consultancy_entry
WHERE id=$1`,
[id]
)
})
}
// (Re)index specific consultancy entry into consultancy search index.
static async indexIntoSearchEngine(entryId, shouldWait) {
const values = [entryId]
const text = `
SELECT
jsonb_strip_nulls(
jsonb_build_object(
'id', ce.id,
'timeCreated', ce.time_created,
'status', ce.status,
'description', ce.description,
'title', ce.title,
'question', ce.question,
'answer', ce.answer,
'answerAuthors', ce.answer_authors,
'primaryDomain', jsonb_strip_nulls(
jsonb_build_object(
'id', dp.id,
'nameSl', dp.name_sl,
'nameEn', dp.name_en
)
),
'assignedConsultants', ARRAY(
SELECT jsonb_strip_nulls(
jsonb_build_object(
'id', u.id,
'firstName', u.first_name,
'lastName', u.last_name,
'isModerator', cec.is_moderator
)
)
FROM consultancy_entry_consultant cec
LEFT JOIN "user" u on u.id = cec.user_id
WHERE cec.entry_id = ce.id
ORDER BY is_moderator DESC
)
)
) entry
FROM consultancy_entry ce
LEFT JOIN domain_primary dp on dp.id = ce.domain_primary_id
WHERE ce.id = $1`
let {
rows: [{ entry }]
} = await db.query(text, values)
if (!entry.answerAuthors?.length) delete entry.answerAuthors
if (!Object.keys(entry.primaryDomain).length) delete entry.primaryDomain
if (!entry.assignedConsultants.length) delete entry.assignedConsultants
entry = removeHtmlTags(JSON.stringify(entry))
await searchEngineClient.index({
id: entryId,
index: CONSULTANCY_ENTRY_INDEX,
body: entry,
refresh: shouldWait ? 'wait_for' : false
})
}
// Remove all consultancy entries from index and index them all again.
static async reindexAll() {
await deleteConsultancyEntriesFromIndex()
const { rows } = await db.query('SELECT id FROM consultancy_entry')
for (const row of rows) {
const { id: entryId } = row
await this.indexIntoSearchEngine(entryId)
}
}
}
module.exports = ConsultancyEntry
+65
View File
@@ -0,0 +1,65 @@
const { Pool, Client } = require('pg')
const debug = require('debug')('termPortal:models/db')
const pool = new Pool()
pool.on('error', (err, client) => {
debug('Error on DB pool idle client:')
debug({ err, client })
})
exports.query = (text, params) => pool.query(text, params)
exports.getClient = () => pool.connect()
// Returns a new client from outside the pool.
exports.getExtraClient = () => {
const client = new Client()
client.connect()
return client
}
exports.transaction = async queriesFn => {
const client = await pool.connect()
try {
await client.query('BEGIN')
await queriesFn(client)
await client.query('COMMIT')
} catch (error) {
await client.query('ROLLBACK')
throw error
} finally {
client.release()
}
}
exports.genParamStr = paramArr => {
const paramPlaceholderString = paramArr.reduce((str, param, index) => {
if (index) str += ', '
str += `$${index + 1}`
return str
}, '')
return paramPlaceholderString
}
exports.waitForConnection = () => {
return new Promise(resolve => {
async function testConnection() {
try {
// eslint-disable-next-line no-console
console.log('Verifying connection to DB server')
await pool.query('SELECT 1')
// eslint-disable-next-line no-console
console.log('Connection to DB server verified')
resolve()
} catch {
// eslint-disable-next-line no-console
console.error('Could not connect to DB server')
setTimeout(testConnection, 1000)
}
}
testConnection()
})
}
+49
View File
@@ -0,0 +1,49 @@
const db = require('./db')
const DemoPaginacija = {}
// Metoda za generacijo demo podatkov.
DemoPaginacija.initDemoData = async () => {
await db.query(`
CREATE TABLE IF NOT EXISTS demo_paginacija (zanimivo TEXT, nezanimivo1 TEXT, nezanimivo2 TEXT);
DO $$
BEGIN
IF (SELECT COUNT(*) FROM demo_paginacija) = 0 THEN
FOR stevec IN 1..9993 LOOP
INSERT INTO demo_paginacija VALUES ('vrednost' || stevec, 'brezveze', 'tega res ne rabimo');
END LOOP;
END IF;
END $$`)
}
// Metoda za poizvedbo demo podatkov za določeno stran.
DemoPaginacija.fetch = 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(
'zanimivo', zanimivo,
'nezanimivo1', nezanimivo1,
'nezanimivo2', nezanimivo2
)
FROM demo_paginacija
LIMIT $1
OFFSET $2
)
) result
`,
[resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
module.exports = DemoPaginacija
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
// TODO Possible duplicate methods in Dictionary model.
// TODO Ambiguous model name. There are also secondary domains, domain labels, ...
const db = require('./db')
class Domain {
constructor({
id,
name_sl: nameSl,
name_en: nameEn,
udk_code: udkCode,
cerif_name: cerifName,
cerif_code: cerifCode,
eurovoc_name: eurovocName,
eurovoc_code: eurovocNode
}) {
this.id = id
this.nameSl = nameSl
this.nameEn = nameEn
this.udkCode = udkCode
this.cerifName = cerifName
this.cerifCode = cerifCode
this.eurovocName = eurovocName
this.eurovocNode = eurovocNode
}
// Fetch all consultancy entries from DB.
static async fetchAll() {
// TODO Luka: Miha, define specific fields instead of using *.
const { rows: fetchedConsEntries } = await db.query(`
SELECT *
FROM domain_primary`)
const domains = fetchedConsEntries.map(domain => new this(domain))
return domains
}
// Fetch domain by ID.
static async fetchById(id) {
// TODO Luka: Miha, define specific fields instead of using *.
const { rows: domainEntity } = await db.query(
`
SELECT *
FROM domain_primary
WHERE id=$1`,
[id]
)
const domain = new this(domainEntity[0])
return domain
}
// Fetch domain by name.
static async fetchByName(name) {
// TODO Luka: Miha, define specific fields instead of using *.
const { rows: domainEntity } = await db.query(
`
SELECT *
FROM domain_primary
WHERE name_sl=$1`,
[name]
)
const domain = new this(domainEntity[0])
return domain
}
// Fetch primary domain id by udk code.
static async fetchIdByUdkCode(udkCode) {
const {
rows: [{ id }]
} = await db.query('SELECT id FROM domain_primary WHERE udk_code = $1', [
udkCode
])
return id
}
}
module.exports = Domain
+40
View File
@@ -0,0 +1,40 @@
const nodemailer = require('nodemailer')
const htmlToText = require('nodemailer-html-to-text').htmlToText()
const {
smtpHost,
smtpPort,
smtpTlsRejectUnauthorized,
smtpFrom
} = require('../config/keys')
const options = {
host: smtpHost,
port: smtpPort,
tls: { rejectUnauthorized: smtpTlsRejectUnauthorized }
}
const defaults = { from: smtpFrom }
const transporter = nodemailer.createTransport(options, defaults)
transporter.use('compile', htmlToText)
exports.send = data => transporter.sendMail(data)
exports.waitForConnection = () => {
return new Promise(resolve => {
async function testConnection() {
try {
// eslint-disable-next-line no-console
console.log('Verifying connection to SMTP server')
await transporter.verify()
// eslint-disable-next-line no-console
console.log('Connection to SMTP server verified')
resolve()
} catch {
// eslint-disable-next-line no-console
console.error('Could not connect to SMTP server')
setTimeout(testConnection, 1000)
}
}
testConnection()
})
}
+650
View File
@@ -0,0 +1,650 @@
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
)
)
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,
'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) => {
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 Luka: I expect "source" needing a rework once linked portals and dictionaries start working.
const source = {
code: await getInstanceSetting('portal_code'),
name: await getInstanceSetting('portal_name')
}
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 }]
} = await db.query(
'SELECT version_snapshot FROM entry_version_history WHERE entry_id = $1 and version = $2',
[entryId, version]
)
return historySnapshot
}
/* 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
+501
View File
@@ -0,0 +1,501 @@
const FormData = require('form-data')
const { createReadStream } = require('fs')
const { writeFile, readFile } = require('fs/promises')
const axios = require('axios')
const db = require('./db')
const {
deserialize,
getDocumentsPath,
getStopTermsPath,
getConllusPath,
getTermCandidatesPath,
getFileNamesInFolder,
getFileStatsInFolder
} = require('./helpers/extraction')
const Extraction = {}
// Fetch all extractions for a specific user.
Extraction.fetchAllForUser = async userId => {
const { rows: fetchedExtractions } = await db.query(
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE user_id = $1 ORDER BY id',
[userId]
)
return fetchedExtractions.map(fetchedExtraction =>
deserialize.extraction(fetchedExtraction)
)
}
// Count all extractions for a specific user.
Extraction.countAllForUser = async userId => {
const {
rows: [{ count: extractionCount }]
} = await db.query('SELECT COUNT(*) FROM extraction WHERE user_id = $1', [
userId
])
return +extractionCount
}
// Create a new (own) extraction entry in DB.
Extraction.createOwn = async (userId, extractionName) => {
const {
rows: [{ id }]
} = await db.query(
'INSERT INTO extraction (user_id, name) VALUES ($1, $2) RETURNING id',
[userId, extractionName]
)
return id
}
// Create a new (oss) extraction entry in DB.
Extraction.createOss = async (userId, extractionName) => {
const {
rows: [{ id }]
} = await db.query(
'INSERT INTO extraction (user_id, name, oss_params) VALUES ($1, $2, $3) RETURNING id',
[userId, extractionName, { params: {}, status: 'new' }]
)
return id
}
// Fetch a specific extraction entry from DB.
Extraction.fetch = async id => {
const {
rows: [fetchedExtraction]
} = await db.query(
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE id = $1',
[id]
)
return deserialize.extraction(fetchedExtraction)
}
// Fetch author email of a specific extraction entry from DB.
Extraction.fetchAuthorEmail = async id => {
const {
rows: [{ email }]
} = await db.query(
`SELECT u.email
FROM extraction e
LEFT JOIN "user" u ON u.id = e.user_id
WHERE e.id = $1`,
[id]
)
return email
}
// Update extraction entry in DB.
Extraction.update = async (id, name) => {
await db.query('UPDATE extraction SET name = $2 WHERE id = $1', [id, name])
}
// Delete a specific extraction entry from DB.
Extraction.delete = async id => {
const {
rows: [{ corpus_id: corpusId }]
} = await db.query(
'DELETE FROM extraction WHERE id = $1 RETURNING corpus_id',
[id]
)
return corpusId
}
// Fetch all documents' names for a specific extraction.
Extraction.fetchAllDocumentsNames = async extractionId => {
const documentsPath = getDocumentsPath(extractionId)
const documentsNames = await getFileNamesInFolder(documentsPath)
return documentsNames
}
// Fetch all documents' metadata for a specific extraction.
Extraction.fetchAllDocumentsStats = async extractionId => {
const documentsPath = getDocumentsPath(extractionId)
const documentsStats = await getFileStatsInFolder(documentsPath)
return documentsStats
}
// Fetch all stop terms files' names for a specific extraction.
Extraction.fetchAllStopTermsFilesNames = async extractionId => {
const stopTermsFilesPath = getStopTermsPath(extractionId)
const stopTermsFilesNames = await getFileNamesInFolder(stopTermsFilesPath)
return stopTermsFilesNames
}
// Fetch all stop terms files' metadata for a specific extraction.
Extraction.fetchAllStopTermsFilesStats = async extractionId => {
const stopTermsFilesPath = getStopTermsPath(extractionId)
const stopTermsFilesStats = await getFileStatsInFolder(stopTermsFilesPath)
return stopTermsFilesStats
}
// Update OSS parameters for a specific extraction in DB.
Extraction.updateOssParams = async (extractionId, newOssParams) => {
await db.query('UPDATE extraction SET oss_params = $1 WHERE id = $2', [
newOssParams,
extractionId
])
}
// Fetch term candidates JSON for a specific extraction.
Extraction.fetchTermCandidatesJson = async extractionId => {
const termCandidatesPath = getTermCandidatesPath(extractionId)
const fileContent = await readFile(termCandidatesPath, 'utf8')
return fileContent
}
// Fetch the number of term candidates for a specific extraction.
Extraction.fetchTermCandidatesCount = async function (extractionId) {
const termCandidatesJson = await this.fetchTermCandidatesJson(extractionId)
const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati
return termCandidates.length
}
// Mark extraction from own documents as began.
Extraction.beginOwn = async (extractionId, documentsNames) => {
let timeStarted
await db.transaction(async dbClient => {
;[
{
rows: [{ time_started: timeStarted }]
}
] = await Promise.all([
dbClient.query(
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
[extractionId]
),
dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, UNNEST($3::VARCHAR[]))',
[extractionId, 'doc to conllu', documentsNames]
),
dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
[extractionId, 'conllus to term candidates', '']
),
dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
[extractionId, 'concordancer', '']
)
])
})
return timeStarted
}
// Mark extraction from OSS as began.
Extraction.beginOss = async extractionId => {
let timeStarted
await db.transaction(async dbClient => {
const insertExtractionJob = dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
[extractionId, 'oss term candidates', '']
)
const updateExtraction = dbClient.query(
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
[extractionId]
)
;[
{
rows: [{ time_started: timeStarted }]
}
] = await Promise.all([updateExtraction, insertExtractionJob])
})
return timeStarted
}
// Fetch all finished extractions for a specific user.
Extraction.fetchFinishedForUser = async userId => {
const { rows: fetchedExtractions } = await db.query(
'SELECT id, name FROM extraction WHERE user_id = $1 AND status = $2 ORDER BY id',
[userId, 'finished']
)
return fetchedExtractions.map(fetchedExtraction =>
deserialize.extraction(fetchedExtraction)
)
}
// Supervise a specific extraction from own documents and bring it out of 'in progress' status.
// This is a temporary solution as explained in its execution context.
// It's also completely unmodular and a complete mess. Refactor at appropriate time.
Extraction.processOwn = async function (extractionId, extractionName) {
// Get documents folder path and all documets' names witin.
const documentsPath = getDocumentsPath(extractionId)
const documentNames = await this.fetchAllDocumentsNames(extractionId)
const conllusPath = getConllusPath(extractionId)
const conllusPaths = []
// Using remote API, transform each document into conllu format.
for (const documentName of documentNames) {
const filePath = `${documentsPath}/${documentName}`
const form = new FormData()
form.append('file', createReadStream(filePath), documentName)
try {
const { data: data1 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/datotekaVConlluAsync',
form,
{
headers: {
...form.getHeaders()
}
}
)
const remotejobId = +data1.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'doc to conllu', documentName]
)
// Kristjan said: I don't have to wait for one job to finish to begin the next. I could launch all at once, which is the whole purpose of async processing.
// Consider reworking it in such manner.
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
)
if (data2.finished_on) {
if (data2.job_status !== 'finished processing (OK)') throw Error()
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
const fileSavePath = `${conllusPath}/${documentName}.conllu`
await writeFile(fileSavePath, data2.job_result)
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'doc to conllu', documentName]
)
conllusPaths.push(fileSavePath)
break
}
}
} catch {
await failTheJob(extractionId, 'doc to conllu', documentName)
}
}
// Conllu transformation for all documents finished. Start extracting term candidates.
// TODO This is a naive implementation which builds the whole payload in memory. Make it streamy.
const conllusArr = []
for (const conlluPath of conllusPaths) {
const fileContent = await readFile(conlluPath, 'utf8')
conllusArr.push(fileContent)
}
const stopTermsPath = getStopTermsPath(extractionId)
const stopTermsFilesNames = await this.fetchAllStopTermsFilesNames(
extractionId
)
const stopTermsSet = new Set()
const stopTermsSeperator = /\r?\n/
for (const stopTermsFileName of stopTermsFilesNames) {
const fileContent = await readFile(
`${stopTermsPath}/${stopTermsFileName}`,
'utf8'
)
const stopTerms = fileContent.split(stopTermsSeperator)
stopTerms.forEach(stopTerm => stopTermsSet.add(stopTerm.trim()))
}
stopTermsSet.delete('')
const termCandidatesPath = getTermCandidatesPath(extractionId)
try {
const { data: data3 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/izlusciAsync',
{
conllus: conllusArr,
prepovedaneBesede: Array.from(stopTermsSet)
}
)
const remotejobId = +data3.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'conllus to term candidates', '']
)
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data4 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
)
if (data4.finished_on) {
if (data4.job_status !== 'finished processing (OK)') throw Error()
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
await writeFile(termCandidatesPath, JSON.stringify(data4.job_result))
// TODO Once returned JSON is properly formed, use the bottom line instead.
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'conllus to term candidates', '']
)
break
}
}
} catch {
await failTheJob(extractionId, 'conllus to term candidates', '')
await failExtraction(extractionId)
return
}
// Now we have conllus and term_candidates.json.
// Start concondancer corpus processing.
try {
await db.query(
"UPDATE extraction_job SET status = 'in progress' WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'concordancer', '']
)
console.log('CREATING CORPUS')
const {
data: {
entityInfo: { id: corpusId }
}
} = await axios.post('http://concordancer:5000/dashboard/corpus', {
title: extractionName
})
console.log('CORPUS CREATED')
console.log('SLEEP FOR 10 SECS')
await sleep(10)
for (const conlluPath of conllusPaths) {
const textPathParts = conlluPath.split('/')
textPathParts[0] = '/data'
const textPath = textPathParts.join('/')
console.log('ADDING TEXT')
await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/text`,
{ sourceFile: textPath }
)
console.log('TEXT ADDED')
console.log('SLEEP FOR 10 SECS')
await sleep(10)
}
const termListPathParts = termCandidatesPath.split('/')
termListPathParts[0] = '/data'
const termListPath = termListPathParts.join('/')
console.log('ADDING TERMS')
await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList`,
{ sourceFile: termListPath }
)
console.log('TERMS ADDED')
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'concordancer', '']
)
await db.query(
"UPDATE extraction SET status = 'finished', time_finished = NOW(), corpus_id = $1 WHERE id = $2",
[corpusId, extractionId]
)
console.log('EXTRACTION SUCCESSFUL')
} catch (e) {
console.log('EXTRACTION ERROR')
console.log(e)
await failTheJob(extractionId, 'concordancer', '')
await failExtraction(extractionId)
}
}
// Supervise a specific extraction from OSS and bring it out of 'in progress' status.
// This is a temporary solution as explained in its execution context.
// It's also completely unmodular and a complete mess. Refactor at appropriate time.
Extraction.processOss = async function (extractionId, ossParams) {
// TODO Consider if it would make sense to make stop term file reading streaming.
// TODO Probably not, at least not while the the OSS enpoint is GET, due to limited length of URLs.
// TODO Also consider refactoring certain parts,
// TODO as some are identical or similar to Own variants or used earlier in the same pipeline.
const stopTermsPath = getStopTermsPath(extractionId)
const stopTermsFilesNames = await this.fetchAllStopTermsFilesNames(
extractionId
)
const stopTermsSet = new Set()
const stopTermsSeperator = /\r?\n/
for (const stopTermsFileName of stopTermsFilesNames) {
const fileContent = await readFile(
`${stopTermsPath}/${stopTermsFileName}`,
'utf8'
)
const stopTerms = fileContent.split(stopTermsSeperator)
stopTerms.forEach(stopTerm => stopTermsSet.add(stopTerm.trim()))
}
stopTermsSet.delete('')
const stopTerms = Array.from(stopTermsSet)
const searchParams = new URLSearchParams({
...(ossParams.year && { leta: ossParams.year }),
...(ossParams.documentType && { vrste: ossParams.documentType }),
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk }),
...(stopTerms.length && { prepovedaneBesede: stopTerms })
})
const extractApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/izlusciPoIskanjuAsync?${searchParams}`
try {
const { data: data1 } = await axios.get(extractApiUrl)
const remotejobId = +data1.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'oss term candidates', '']
)
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
)
if (data2.finished_on) {
if (data2.job_status !== 'finished processing (OK)') throw Error()
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
const termCandidatesPath = getTermCandidatesPath(extractionId)
await writeFile(termCandidatesPath, JSON.stringify(data2.job_result))
// TODO Once returned JSON is properly formed, use the bottom line instead.
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'oss term candidates', '']
)
break
}
}
// Now we have term_candidates.json.
// Mark extraction as finished.
await db.query(
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
[extractionId]
)
} catch {
await failTheJob(extractionId, 'oss term candidates', '')
await failExtraction(extractionId)
}
}
async function failTheJob(extractionId, jobType, documentName) {
await db.query(
"UPDATE extraction_job SET status = 'failed', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, jobType, documentName]
)
}
async function failExtraction(extractionId) {
await db.query(
"UPDATE extraction SET status = 'failed', time_finished = NOW() WHERE id = $1",
[extractionId]
)
}
function sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}
module.exports = Extraction
@@ -0,0 +1,282 @@
const fs = require('fs')
const xmlFlow = require('xml-flow')
const xss = require('xss')
const debug = require('debug')('termPortal:models/helpers/dictionary')
const db = require('../../db')
const { intoDbArray } = require('..')
const JOBS_MAX = 50
const JOBS_MIN = 15
// Import given file into given dictionary.
exports.readFileIntoDb = (
userId,
dictionaryId,
importFilePath,
entryStatus,
dbClient
) => {
return new Promise((resolve, reject) => {
const progress = {
jobCount: 0,
totalCount: 0,
validCount: 0,
isWholeFileRead: false,
importError: null
}
entryStatus = entryStatus === 'complete' ? 'complete' : 'in_edit'
const importFileReader = fs.createReadStream(importFilePath)
const xmlStream = xmlFlow(importFileReader, {
strict: true,
trim: false,
preserveMarkup: xmlFlow.ALWAYS,
simplifyNodes: false,
useArrays: xmlFlow.ALWAYS
})
const handleError = handleImportError(xmlStream, progress, reject)
const handleEntry = handleEntryXml(
userId,
dictionaryId,
entryStatus,
progress,
dbClient,
resolve,
handleError
)
xmlStream
.on('error', () => {})
.once('error', handleError)
.on('end', () => (progress.isWholeFileRead = true))
.on('tag:entry', handleEntry)
})
}
function handleImportError(xmlStream, progress, reject) {
return function handleError(error) {
if (progress.importError) return
progress.importError = error
xmlStream.removeAllListeners('tag:entry')
reject(error)
}
}
function handleEntryXml(
userId,
dictionaryId,
entryStatus,
progress,
dbClient,
resolve,
handleError
) {
return async function handleEntry(entry) {
if (progress.importError) return
try {
if (++progress.jobCount > JOBS_MAX) this.pause()
const term = toMixedBasic(
entry.$markup.find(el => el.$name === 'term')?.$markup
)
const hwGrp = entry.$markup.find(el => el.$name === 'hwGrp')
const wordforms = hwGrp?.$attrs.wfs
const accent = hwGrp?.$attrs.acc
const pronunciation = hwGrp?.$attrs.pron
const domainLabels = entry.$markup
.find(el => el.$name === 'domainLabels')
?.$markup.filter(childEl => childEl.$name === 'domainLabel')
.map(domainLabel => toText(domainLabel.$markup))
const label = toMixedExtended(
entry.$markup.find(el => el.$name === 'label')?.$markup
)
const definition = toMixedExtended(
entry.$markup.find(el => el.$name === 'def')?.$markup
)
const synonyms = entry.$markup
.find(el => el.$name === 'syns')
?.$markup.filter(childEl => childEl.$name === 'syn')
.map(synonym => toMixedBasic(synonym.$markup))
const links = entry.$markup
.find(el => el.$name === 'links')
?.$markup.filter(childEl => childEl.$name === 'link')
.map(link => ({
link: toMixedBasic(link.$markup),
type: link.$attrs.type
}))
const other = toMixedOther(
entry.$markup.find(el => el.$name === 'other')?.$markup
)
let hasForeignTerms = false
const foreignLanguageContent = entry.$markup
.find(el => el.$name === 'fLangs')
?.$markup.filter(childEl => childEl.$name === 'fLang')
.map(fLang => ({
language: fLang.$attrs.lang,
terms: fLang.$markup
.find(el => el.$name === 'fTerms')
?.$markup.filter(childEl => childEl.$name === 'fTerm')
.map(term => {
const content = toMixedBasic(term.$markup)
if (content.length) hasForeignTerms = true
return content
}),
definition:
toMixedExtended(
fLang.$markup.find(el => el.$name === 'fDef')?.$markup
) || null,
synonyms: fLang.$markup
.find(el => el.$name === 'fSyns')
?.$markup.filter(childEl => childEl.$name === 'fSyn')
.map(synonym => toMixedBasic(synonym.$markup))
}))
const multimedia = entry.$markup.find(el => el.$name === 'mm')?.$markup
const images = []
const audio = []
const videos = []
multimedia?.forEach(mm => {
switch (mm.$name) {
case 'image':
images.push(toText(mm.$markup))
break
case 'audio':
audio.push(toText(mm.$markup))
break
case 'video':
videos.push(toText(mm.$markup))
}
})
const isValid = !!term && (!!definition || hasForeignTerms)
if (!entry.$markup.length) return
const values = [
dictionaryId,
isValid,
entryStatus,
term || null,
userId,
null,
wordforms,
accent,
pronunciation,
intoDbArray(domainLabels, 'always'),
label || null,
definition || null,
intoDbArray(synonyms),
intoDbArray(links, 'always'),
other || null,
intoDbArray(foreignLanguageContent, 'always'),
images.length ? images : null,
audio.length ? audio : null,
videos.length ? videos : null
]
const text = `SELECT entry_new (${db.genParamStr(values)})`
await dbClient.query(text, values)
progress.totalCount++
if (isValid) progress.validCount++
} catch (error) {
handleError(error)
} finally {
progress.jobCount--
if (progress.jobCount < JOBS_MIN) this.resume()
if (
!progress.jobCount &&
progress.isWholeFileRead &&
!progress.importError
) {
debug(`TOTAL ENTRIES: ${progress.totalCount}`)
debug(`VALID ENTRIES: ${progress.validCount}`)
resolve()
}
}
}
}
const markupFilter = {
noMixed: new xss.FilterXSS({
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedBasic: new xss.FilterXSS({
whiteList: {
sup: [],
sub: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedExtended: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href']
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
}),
mixedOther: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href'],
br: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
})
}
function customTagHandler(tag, html, { isWhite, isClosing }) {
// Special treatment only for whitelisted opening anchor tags.
if (tag !== 'a' || !isWhite || isClosing) return
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
return `<a href${url ? `="${url}" target="_blank"` : ''}>`
}
function toText(markupObj) {
return markupFilter.noMixed
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedBasic(markupObj) {
return markupFilter.mixedBasic
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedExtended(markupObj) {
return markupFilter.mixedExtended
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedOther(markupObj) {
return markupFilter.mixedOther
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
+196
View File
@@ -0,0 +1,196 @@
const { removeHtmlTags } = require('../../helpers')
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
exports.deserialize = {
primaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
secondaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
isApproved: domain.approved,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
approvedSecondaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
language(language) {
const deserializedLanguage = {
id: language.id,
code: language.code,
nameSl: language.name_sl,
nameEn: language.name_en
}
return deserializedLanguage
},
editDescription(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
nameEn: dictionary.name_en,
nameSlShort: dictionary.name_sl_short,
author: dictionary.author,
domainPrimary: dictionary.domain_primary_id,
description: dictionary.description,
issn: dictionary.issn
}
return deserializedDictionary
},
editUsers(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
terminologyReviewFlag: dictionary.entries_have_terminology_review_flag,
languageReviewFlag: dictionary.entries_have_language_review_flag,
status: dictionary.status
}
return deserializedDictionary
},
editStructure(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
hasDomainLabels: dictionary.entries_have_domain_labels,
hasLabel: dictionary.entries_have_label,
hasDefinition: dictionary.entries_have_definition,
hasSynonyms: dictionary.entries_have_synonyms,
hasLinks: dictionary.entries_have_links,
hasOther: dictionary.entries_have_other,
hasForeignLanguages: dictionary.entries_have_foreign_languages,
hasForeignDefinitions: dictionary.entries_have_foreign_definitions,
hasForeignSynonyms: dictionary.entries_have_foreign_synonyms,
hasImages: dictionary.entries_have_images,
hasAudio: dictionary.entries_have_audio,
hasVideo: dictionary.entries_have_videos
}
return deserializedDictionary
},
editDomainLabels(domainLabel) {
const deserializedDomainLabel = {
id: domainLabel.id,
name: domainLabel.name,
isVisible: domainLabel.is_visible
}
return deserializedDomainLabel
},
imports(oneImport) {
const deserializedImports = {
timeStarted: oneImport.time_started,
status: oneImport.status,
deleteExisting: oneImport.delete_existing_entries,
fileFormat: oneImport.file_format,
countValidEntries: oneImport.count_valid_entries
}
return deserializedImports
}
}
exports.bulkIndex = async (entries, primaryDomain, dictionary, source) => {
const entriesCount = entries.length
if (!entries.length) return
// Construct request body.
const bulkBody = new Array(entriesCount * 2)
for (let i = 0; i < entriesCount; i++) {
let entry = prepareEntryForIndexing(entries[i])
bulkBody[i * 2] = { index: { _index: ENTRY_INDEX, _id: entry.id } }
entry.primaryDomain = primaryDomain
entry.dictionary = dictionary
entry.source = source
entry = removeHtmlTags(JSON.stringify(entry))
bulkBody[i * 2 + 1] = entry
}
// Send the request.
const bulkResponse = await searchEngineClient.bulk({ body: bulkBody })
// Log possible errors.
if (bulkResponse.errors) {
const erroredDocuments = []
bulkResponse.items.forEach((action, i) => {
const operation = Object.keys(action)[0]
if (action[operation].error) {
erroredDocuments.push({
// If the status is 429 it means that you can retry the document,
// otherwise it's very likely a mapping error, and you should
// fix the document before to try it again.
status: action[operation].status,
error: action[operation].error,
operation: action[operation].body[i * 2],
document: action[operation].body[i * 2 + 1]
})
}
})
console.log('Errors indexing documents:') // eslint-disable-line no-console
console.log(erroredDocuments) // eslint-disable-line no-console
}
}
function prepareEntryForIndexing(entry) {
// Snake case property names into camel case.
entry.isValid = entry.is_valid
delete entry.is_valid
entry.isPublished = entry.is_published
delete entry.is_published
entry.isTerminologyReviewed = entry.is_terminology_reviewed
delete entry.is_terminology_reviewed
entry.isLanguageReviewed = entry.is_language_reviewed
delete entry.is_language_reviewed
entry.homonymSort = entry.homonym_sort
delete entry.homonym_sort
entry.timeMostRecentComment = entry.time_most_recent_comment
delete entry.time_most_recent_comment
entry.domainLabels = entry.domain_labels
delete entry.domain_labels
entry.foreignEntries = entry.foreign_entries
delete entry.foreign_entries
// Remove (top-level) properies with null or empty array values.
entry = Object.fromEntries(
Object.entries(entry).filter(
([_, v]) => v !== null && (!Array.isArray(v) || v.length)
)
)
return entry
}
exports.prepareEntryForIndexing = prepareEntryForIndexing
+62
View File
@@ -0,0 +1,62 @@
const { readdir, stat } = require('fs/promises')
const { partial } = require('filesize')
const { DATA_FILES_PATH } = require('../../config/settings')
const formatFileSize = partial({ separator: ',' })
exports.deserialize = {
extraction(extraction) {
const deserializedExtraction = {
id: extraction.id,
name: extraction.name,
status: extraction.status,
corpusId: extraction.corpus_id,
timeStarted: extraction.time_started,
timeFinished: extraction.time_finished,
ossParams: extraction.oss_params
}
return deserializedExtraction
}
}
exports.getExtractionFilesPath = getExtractionFilesPath
exports.getDocumentsPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/documents`
}
exports.getStopTermsPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/stop_terms`
}
exports.getConllusPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/conllu`
}
exports.getTermCandidatesPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/term_candidates.json`
}
exports.getFileNamesInFolder = async folderPath => {
const filenames = await readdir(folderPath)
return filenames
}
exports.getFileStatsInFolder = async folderPath => {
const filenames = await readdir(folderPath)
const fileStats = await Promise.all(
filenames.map(async filename => {
const filePath = `${folderPath}/${filename}`
const { mtimeMs: timeModified, size } = await stat(filePath)
const sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats
})
)
return fileStats
}
function getExtractionFilesPath(extractionId) {
return `${DATA_FILES_PATH}/extraction/${extractionId}`
}
+73
View File
@@ -0,0 +1,73 @@
const xss = require('xss')
const cache = require('../cache')
const Portal = require('../portal')
const INSTANCE_SETTING_NAMESPACE = 'instance-setting'
const INSTANCE_SETTING_CACHE_EXPIRE_TIME = 60 * 60 // 1 hour.
const SLOVENIAN_LANGUAGE_ID_CACHE_EXPIRE_TIME = 60 * 60 // 1 hour.
exports.intoDbArray = (inputData, mode) => {
if (Array.isArray(inputData)) {
return inputData
} else if (inputData === undefined || inputData === '') {
if (mode === 'always') return []
if (mode === 'undefined') return undefined
else return null
// return mode === 'always' ? [] : null
}
return [inputData]
}
exports.removeHtmlTags = text => {
return tagFilter.process(text)
}
// Returns setting value by its name from table instance_settings.
exports.getInstanceSetting = async settingName => {
const settingCacheName = `${INSTANCE_SETTING_NAMESPACE}:${settingName}`
let settingValue = await cache.get(settingCacheName)
if (!settingValue) {
settingValue = await Portal.getInstanceSettingValue(settingName)
await cache.set(
settingCacheName,
settingValue,
'EX',
INSTANCE_SETTING_CACHE_EXPIRE_TIME
)
}
return settingValue
}
// Deletes cached instance settings.
exports.clearCachedInstanceSettings = async () => {
const settingNames = await Portal.fetchAllInstanceSettingNames()
const settingCacheNames = settingNames.map(
settingName => `${INSTANCE_SETTING_NAMESPACE}:${settingName}`
)
await cache.unlink(...settingCacheNames)
}
// Returns slovenian language id and keeps it cached.
exports.getSlovenianLanguageId = async () => {
const settingCacheName = 'slovenian-language-id'
let id = await cache.get(settingCacheName)
if (!id) {
id = (await Portal.getSlovenianLanguageId()).toString()
await cache.set(
settingCacheName,
id,
'EX',
SLOVENIAN_LANGUAGE_ID_CACHE_EXPIRE_TIME
)
}
return id
}
const tagFilter = new xss.FilterXSS({
whiteList: {},
stripIgnoreTag: true
})
+67
View File
@@ -0,0 +1,67 @@
exports.aggregateSettings = settings => {
const deserializedSettings = settings.reduce((agg, setting) => {
agg[setting.name] = setting.value
return agg
}, {})
return deserializedSettings
}
exports.deserialize = {
settings(settings) {
const deserializedSettings = {
name: settings.portal_name,
description: settings.portal_description,
code: settings.portal_code,
isExtractionEnabled: settings.is_extraction_enabled,
isDictionariesEnabled: settings.is_dictionaries_enabled,
isConsultancyEnabled: settings.is_consultancy_enabled
}
return deserializedSettings
},
dictSettings(settings) {
const deserializedSettings = {
minEntriesPerDictionary: settings.min_entries_per_dictionary,
dictionaryPublishApproval: settings.dictionary_publish_approval,
// keepNumOfExportsPerDict: settings.keep_num_of_exports_per_dictionary,
// dictionaryAutoSaveFrequency: settings.dictionary_auto_save_frequency,
numOfHistoryEntriesPerEntry: settings.num_of_history_entires_per_entry,
canPublishEntriesInEdit: settings.can_publish_entries_in_edit
}
return deserializedSettings
},
consultSettings(settings) {
const deserializedSettings = {
consultancyType: settings.consultancy_type,
zrcEmail: settings.zrc_email,
zrcURL: settings.zrc_url
}
return deserializedSettings
},
connections(connection) {
const deserializedConnections = {
id: connection.id,
name: connection.name,
indexURL: connection.url_index,
code: connection.code,
isEnabled: connection.is_enabled,
synced: connection.time_last_synced,
isLinked: connection.is_linked,
URLupdate: connection.url_update
}
return deserializedConnections
},
dictionaries(dictionary) {
const deserializeDictionaries = {
id: dictionary.id,
name: dictionary.name,
code: dictionary.code,
isEnabled: dictionary.is_enabled
}
return deserializeDictionaries
}
}
@@ -0,0 +1,40 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,76 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match_phrase: {
title: searchString
}
},
boost: 3
}
},
{
constant_score: {
filter: {
match_phrase: {
question: searchString
}
},
boost: 2
}
},
{
constant_score: {
filter: {
match_phrase: {
answer: searchString
}
},
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,70 @@
const { queryForField } = require('../..')
const whitespace = /\s+/
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const searchTokens = searchString.split(whitespace)
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: queryForField('title', searchTokens),
boost: 3
}
},
{
constant_score: {
filter: queryForField('question', searchTokens),
boost: 2
}
},
{
constant_score: {
filter: queryForField('answer', searchTokens),
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,85 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match: {
title: {
query: searchString,
operator: 'and'
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
match: {
question: {
query: searchString,
operator: 'and'
}
}
},
boost: 2
}
},
{
constant_score: {
filter: {
match: {
answer: {
query: searchString,
operator: 'and'
}
}
},
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,129 @@
const { EDITOR_MAX_HITS } = require('../../../../../config/settings')
module.exports = function (dictionaryId, filters, searchFieldFilters) {
const queryDsl = {
_source: ['id', 'isValid', 'isPublished', 'term'],
fields: ['foreignEntries.terms'],
script_fields: {
commentActivityIndicator: {
script: {
source: `
if (!doc.containsKey('timeMostRecentComment') || doc['timeMostRecentComment'].empty) return "";
ZonedDateTime mostRecentCommentTime = doc['timeMostRecentComment'].value;
long nowMilli = params['now'];
Instant nowInstant = Instant.ofEpochMilli(nowMilli);
ZonedDateTime now = ZonedDateTime.ofInstant(nowInstant, ZoneId.of('Z'));
long ageInDays = ChronoUnit.DAYS.between(mostRecentCommentTime, now);
if (ageInDays < 7) {
return "T"
} else if (ageInDays < 30) {
return "M"
} else if (ageInDays < 365) {
return "L"
} else {
return ""
}
`,
params: {
now: Date.now()
}
}
}
},
size: EDITOR_MAX_HITS,
query: {
bool: {
filter: [
{
term: {
'dictionary.id': dictionaryId
}
}
]
}
},
sort: ['term.sort', 'homonymSort']
}
if (searchFieldFilters) queryDsl.query.bool.filter.push(searchFieldFilters)
if (filters) {
if (filters.isValid !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isValid: filters.isValid
}
})
}
if (filters.isPublished !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isPublished: filters.isPublished
}
})
}
if (filters.hasComments !== undefined) {
if (filters.hasComments === true) {
queryDsl.query.bool.filter.push({
exists: {
field: 'timeMostRecentComment'
}
})
} else {
queryDsl.query.bool.filter.push({
bool: {
must_not: {
exists: {
field: 'timeMostRecentComment'
}
}
}
})
}
}
if (filters.isComplete !== undefined) {
if (filters.isComplete === true) {
queryDsl.query.bool.filter.push({
term: {
status: 'complete'
}
})
} else {
queryDsl.query.bool.filter.push({
bool: {
must_not: {
term: {
status: 'complete'
}
}
}
})
}
}
if (filters.isTerminologyReviewed !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isTerminologyReviewed: filters.isTerminologyReviewed
}
})
}
if (filters.isLanguageReviewed !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isLanguageReviewed: filters.isLanguageReviewed
}
})
}
}
return queryDsl
}
@@ -0,0 +1,103 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, filters) {
const searchFieldFilters = generateSearchFieldFilters(searchField)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
exists: {
field: mappedName
}
}
}
}
}
return {
exists: {
field: mappedName
}
}
}
return {
dis_max: {
queries: [
{
exists: {
field: 'term'
}
},
{
exists: {
field: 'synonyms'
}
},
{
exists: {
field: 'label'
}
},
{
exists: {
field: 'definition'
}
},
{
exists: {
field: 'other'
}
},
{
exists: {
field: 'domainLabels'
}
},
{
exists: {
field: 'links'
}
},
{
nested: {
path: 'foreignEntries',
query: {
dis_max: {
queries: [
{
exists: {
field: 'foreignEntries.terms'
}
},
{
exists: {
field: 'foreignEntries.synonyms'
}
},
{
exists: {
field: 'foreignEntries.definition'
}
}
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,76 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
[mappedName]: searchString
}
}
}
}
}
return {
match_phrase: {
[mappedName]: searchString
}
}
}
return {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,62 @@
const { fieldMap, queryForField } = require('../..')
const generateQuery = require('./all')
const whitespace = /\s+/
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const searchTokens = searchString.split(whitespace)
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: queryForField(mappedName, searchTokens)
}
}
}
return queryForField(mappedName, searchTokens)
}
return {
dis_max: {
queries: [
queryForField('term', searchTokens),
queryForField('synonyms', searchTokens),
queryForField('label', searchTokens),
queryForField('definition', searchTokens),
queryForField('other', searchTokens),
queryForField('domainLabels', searchTokens),
queryForField('links', searchTokens),
{
nested: {
path: 'foreignEntries',
query: {
dis_max: {
queries: [
queryForField('foreignEntries.terms', searchTokens),
queryForField('foreignEntries.synonyms', searchTokens),
queryForField('foreignEntries.definition', searchTokens)
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,82 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
match: {
[mappedName]: {
query: searchString,
operator: 'and'
}
}
}
}
}
}
return {
match: {
[mappedName]: {
query: searchString,
operator: 'and'
}
}
}
}
return {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,183 @@
const debug = require('debug')('termPortal:generate-query')
const genAllHitQuery = require('./main/all')
const genAllAggQuery = require('./main/aggregate-all')
const genPhraseHitQuery = require('./main/phrase')
const genPhraseAggQuery = require('./main/aggregate-phrase')
const genWildcardHitQuery = require('./main/wildcard')
const genWildcardAggQuery = require('./main/aggregate-wildcard')
const genMultiWordHitQuery = require('./main/word-multi')
const genWordAggQuery = require('./main/aggregate-word')
const genSingleWordHitQuery = require('./main/word-single')
const genEditorAllQuery = require('./editor/all')
const genEditorExistsQuery = require('./editor/exists')
const genEditorPhraseQuery = require('./editor/phrase')
const genEditorWildcardQuery = require('./editor/wildcard')
const genEditorWordsQuery = require('./editor/words')
const genConsultancyAllQuery = require('./consultancy/all')
const genConsultancyPhraseQuery = require('./consultancy/phrase')
const genConsultancyWildcardQuery = require('./consultancy/wildcard')
const genConsultancyWordsQuery = require('./consultancy/words')
const notOnlyAsterisks = /[^*"\s]/
const hasWildcards = /[*?]/
const hasWhitespace = /\s/
// Determines query type, constructs and returns appropriate search engine query(es).
exports.main = async (
searchString,
filters,
hitsPerPage,
page,
withAggregation
) => {
let hitsQuery
let aggregateQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!notOnlyAsterisks.test(searchString)) {
debug('query type: all')
hitsQuery = genAllHitQuery(filters, hitsPerPage, page)
if (withAggregation) {
aggregateQuery = genAllAggQuery(hitsQuery)
}
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = await genPhraseHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genPhraseAggQuery(searchString, hitsQuery)
}
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
let slovenianFieldQueries, foreignFieldQueries
;[hitsQuery, slovenianFieldQueries, foreignFieldQueries] =
await genWildcardHitQuery(searchString, filters, hitsPerPage, page)
if (withAggregation) {
aggregateQuery = genWildcardAggQuery(
hitsQuery,
slovenianFieldQueries,
foreignFieldQueries
)
}
} else if (hasWhitespace.test(searchString)) {
debug('query type: multi word')
hitsQuery = await genMultiWordHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genWordAggQuery(searchString, hitsQuery)
}
} else {
debug('query type: single word')
hitsQuery = await genSingleWordHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genWordAggQuery(searchString, hitsQuery)
}
}
return withAggregation ? [hitsQuery, aggregateQuery] : hitsQuery
}
// Determines query type, constructs and returns appropriate search engine query for editor.
exports.editor = (dictionaryId, searchField, searchString, filters) => {
let hitsQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!searchString) {
debug('query type: all')
hitsQuery = genEditorAllQuery(dictionaryId, filters)
} else if (!notOnlyAsterisks.test(searchString)) {
debug('query type: exists')
hitsQuery = genEditorExistsQuery(dictionaryId, searchField, filters)
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = genEditorPhraseQuery(
dictionaryId,
searchField,
searchString,
filters
)
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
hitsQuery = genEditorWildcardQuery(
dictionaryId,
searchField,
searchString,
filters
)
} else {
debug('query type: words')
hitsQuery = genEditorWordsQuery(
dictionaryId,
searchField,
searchString,
filters
)
}
return hitsQuery
}
// Determines query type, constructs and returns appropriate search engine query for consultancy.
exports.consultancy = (searchString, filters, hitsPerPage, page) => {
let hitsQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!notOnlyAsterisks.test(searchString)) {
debug('query type: all')
hitsQuery = genConsultancyAllQuery(filters, hitsPerPage, page)
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = genConsultancyPhraseQuery(
searchString,
filters,
hitsPerPage,
page
)
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
hitsQuery = genConsultancyWildcardQuery(
searchString,
filters,
hitsPerPage,
page
)
} else {
debug('query type: words')
hitsQuery = genConsultancyWordsQuery(
searchString,
filters,
hitsPerPage,
page
)
}
return hitsQuery
}
@@ -0,0 +1,42 @@
module.exports = function (hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,80 @@
module.exports = function (searchString, hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,68 @@
module.exports = function (
hitsQuery,
slovenianFieldQueries,
foreignFieldQueries
) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
dis_max: {
queries: slovenianFieldQueries
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
dis_max: {
queries: foreignFieldQueries
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,80 @@
module.exports = function (searchString, hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,64 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
return queryDsl
}
@@ -0,0 +1,247 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
term: {
'term.keyword': {
value: searchString
}
}
},
boost: 6
}
},
{
constant_score: {
filter: {
match_phrase: {
term: searchString
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
term: {
'foreignEntries.terms.keyword': {
value: searchString
}
}
}
}
},
boost: 4
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.terms': searchString
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match_phrase: {
synonyms: searchString
}
},
{
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.synonyms': searchString
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.definition': searchString
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
@@ -0,0 +1,207 @@
const { separateSlovenianLanguage, queryForField } = require('../..')
const whitespace = /\s+/
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const searchTokens = searchString.split(whitespace)
const termQuery = queryForField('term', searchTokens)
const foreignTermQuery = queryForField('foreignEntries.terms', searchTokens)
const synonymsQuery = queryForField('synonyms', searchTokens)
const foreignSynonymsQuery = queryForField(
'foreignEntries.synonyms',
searchTokens
)
const labelQuery = queryForField('label', searchTokens)
const definitionQuery = queryForField('definition', searchTokens)
const otherQuery = queryForField('other', searchTokens)
const domainLabelsQuery = queryForField('domainLabels', searchTokens)
const linksQuery = queryForField('links', searchTokens)
const foreignDefinitionQuery = queryForField(
'foreignEntries.definition',
searchTokens
)
const slovenianFieldQueries = [
termQuery,
synonymsQuery,
labelQuery,
definitionQuery,
otherQuery,
domainLabelsQuery,
linksQuery
]
const foreignFieldQueries = [
foreignTermQuery,
foreignSynonymsQuery,
foreignDefinitionQuery
]
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: termQuery,
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: foreignTermQuery
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
synonymsQuery,
{
nested: {
path: 'foreignEntries',
query: foreignSynonymsQuery
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
labelQuery,
definitionQuery,
otherQuery,
domainLabelsQuery,
linksQuery,
{
nested: {
path: 'foreignEntries',
query: foreignDefinitionQuery
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push(...slovenianFieldQueries)
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
dis_max: {
queries: foreignFieldQueries
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return [queryDsl, slovenianFieldQueries, foreignFieldQueries]
}
@@ -0,0 +1,233 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match: {
term: {
query: searchString,
operator: 'and'
}
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.terms': {
query: searchString,
operator: 'and'
}
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match: {
synonyms: {
query: searchString,
operator: 'and'
}
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.synonyms': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.definition': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
@@ -0,0 +1,262 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
term: {
'term.keyword': {
value: searchString
}
}
},
boost: 6
}
},
{
constant_score: {
filter: {
match: {
term: {
query: searchString,
operator: 'and'
}
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
term: {
'foreignEntries.terms.keyword': {
value: searchString
}
}
}
}
},
boost: 4
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.terms': {
query: searchString,
operator: 'and'
}
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match: {
synonyms: {
query: searchString,
operator: 'and'
}
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.synonyms': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.definition': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
+225
View File
@@ -0,0 +1,225 @@
const Portal = require('../../portal')
const { getSlovenianLanguageId } = require('..')
const slCollator = new Intl.Collator('sl', { sensitivity: 'base' })
// Return a new array, with slovenian language removed and isSlovenianSource set to true if present.
exports.separateSlovenianLanguage = async languages => {
const slovenianLanguageId = await getSlovenianLanguageId()
let isSlovenianSource = false
const foreignLanguages = languages.filter(language => {
if (language !== slovenianLanguageId) return true
isSlovenianSource = true
return false
})
return [foreignLanguages, isSlovenianSource]
}
// Simplify and enhance search engine's hits output into useful entries object.
exports.prepareEntries = hits => {
const entriesByCategory = hits.body.hits.hits.reduce(
(agg, hit) => {
const entry = hit._source
switch (hit._score) {
case 6:
case 5:
agg.byTerm.push(entry)
break
case 4:
case 3:
agg.byForeignTerm.push(entry)
break
case 2:
case 1:
agg.byOther.push(entry)
break
default:
agg.uncategorized.push(entry)
}
return agg
},
{ byTerm: [], byForeignTerm: [], byOther: [], uncategorized: [] }
)
return entriesByCategory
}
// Transform search engine's aggregation raw output into correct and friendly format.
exports.prepareAggregation = async aggregationRaw => {
const { aggregations, hits } = aggregationRaw.body
const hitsCount = hits.total.value
if (!hitsCount) return
const primaryDomainIds = aggregations.primaryDomains.buckets.map(
bucket => bucket.key
)
const dictionaryIds = aggregations.dictionaries.buckets.map(
bucket => bucket.key
)
const languageIdSet = new Set()
const refinedSourceLanguageBuckets = []
aggregations.foreign.targetLanguages.buckets.forEach(bucket =>
languageIdSet.add(bucket.key)
)
const slovenianHitCount = aggregations.slovenianHits?.doc_count
// "Match all" aggregation returns no slovenian hits and source languages.
if (slovenianHitCount) {
const slovenianLanguageId = await getSlovenianLanguageId()
const slovenianBucket = {
key: slovenianLanguageId,
doc_count: slovenianHitCount
}
let wasSlovenianBucketInserted = false
aggregations.foreign.foreignHits.sourceLanguages.buckets.forEach(bucket => {
if (!wasSlovenianBucketInserted && bucket.doc_count < slovenianHitCount) {
languageIdSet.add(slovenianLanguageId)
refinedSourceLanguageBuckets.push(slovenianBucket)
wasSlovenianBucketInserted = true
}
languageIdSet.add(bucket.key)
refinedSourceLanguageBuckets.push(bucket)
})
if (!wasSlovenianBucketInserted) {
languageIdSet.add(slovenianLanguageId)
refinedSourceLanguageBuckets.push(slovenianBucket)
}
} else if (slovenianHitCount === 0) {
aggregations.foreign.foreignHits.sourceLanguages.buckets.forEach(bucket => {
languageIdSet.add(bucket.key)
refinedSourceLanguageBuckets.push(bucket)
})
}
const languageIds = [...languageIdSet]
const names = await Portal.getSearchAggregateNames(
primaryDomainIds,
dictionaryIds,
languageIds
)
const aggregation = {
sourceLanguages: refinedSourceLanguageBuckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.languages[id], hits }
}
),
targetLanguages: aggregations.foreign.targetLanguages.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.languages[id], hits }
}
),
primaryDomains: aggregations.primaryDomains.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.primaryDomains[id], hits }
}
),
dictionaries: aggregations.dictionaries.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.dictionaries[id], hits }
}
),
sources: aggregations.sources.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: id, hits }
}
)
}
sortAggregation(aggregation)
return aggregation
}
// Enhance aggregation for display in search filters.
exports.prepareSeachFilterData = (aggregation, filters) => {
const enhancedAggregation = {}
if (aggregation) {
for (const category of Object.keys(aggregation)) {
const enhancedCategoryMembers = aggregation[category].map(member => {
if (filters[category].includes(member.id)) member.checked = true
return member
})
enhancedAggregation[category] = enhancedCategoryMembers
}
}
return enhancedAggregation
}
// Maps editor search api field names into search engine ones.
exports.fieldMap = {
term: 'term',
synonyms: 'synonyms',
label: 'label',
definition: 'definition',
other: 'other',
domainLabels: 'domainLabels',
links: 'links',
foreignTerms: 'foreignEntries.terms',
foreignSynonyms: 'foreignEntries.synonyms',
foreignDefinition: 'foreignEntries.definition'
}
// Generate search engine DSL wildcard query fragment for a single field.
exports.queryForField = (field, searchTokens) => {
return {
bool: {
filter: searchTokens.map(token => {
return {
wildcard: {
[field]: {
value: token
}
}
}
})
}
}
}
// Simplify and enhance search engine's editor hits output into useful entries list.
exports.prepareEditorEntries = hits => {
const entries = hits.body.hits.hits.map(hit => {
const entry = hit._source
entry.foreignTerm = hit.fields['foreignEntries.terms']?.[0]
entry.commentActivityIndicator = hit.fields.commentActivityIndicator[0]
return entry
})
return entries
}
function sortAggregation(aggregation) {
for (const categoryBuckets of Object.values(aggregation)) {
categoryBuckets.sort(bucketCompareFn)
}
}
function bucketCompareFn(bucketA, bucketB) {
if (bucketA.hits !== bucketB.hits) return 0
return slCollator.compare(bucketA.name, bucketB.name)
}
// Simplify search engine's consultancy hits output into useful entries list.
exports.prepareConsultancyEntries = hits => {
const entries = hits.body.hits.hits.map(hit => hit._source)
return entries
}
+42
View File
@@ -0,0 +1,42 @@
exports.deserialize = {
userById(user) {
const deserializedUser = {
id: user.id,
userName: user.username,
firstName: user.first_name,
lastName: user.last_name,
email: user.email,
hitsPerPage: user.hits_per_page,
userRoles: user.user_roles,
assignedConsultancyEntries: user.assigned_consultancy_entries
}
return deserializedUser
},
user(userData) {
const deserializedData = {
id: userData.id,
userName: userData.username,
firstName: userData.first_name,
lastName: userData.last_name,
email: userData.email,
password: userData.password
}
return deserializedData
},
userRights(userData) {
const deserializedRights = {
id: userData.id,
userName: userData.username,
email: userData.email,
hasAdministration: userData.administration,
hasEditing: userData.editing,
hasTerminologyReview: userData.terminology_review,
hasLanguageReview: userData.language_review
}
return deserializedRights
}
}
+109
View File
@@ -0,0 +1,109 @@
const db = require('./db')
// const debug = require('debug')('termPortal:sync')
// Fetch all own published dictionaries from DB.
class InterInstanceSync {
static async listDictionaries() {
const indexSql = `
SELECT
d.id,
d.name_sl,
d.name_sl_short,
d.name_en,
d.author,
d.issn,
dp.name_sl domain_primary_name_sl,
ARRAY(
SELECT jsonb_build_object(
'name_sl', ds.name_sl,
'name_en', ds.name_en
)
FROM dictionary_domain_secondary dds
LEFT JOIN domain_secondary ds ON ds.id = dds.domain_secondary_id
WHERE dds.dictionary_id = d.id
) domain_secondary_names,
ARRAY(
SELECT l.code
FROM dictionary_language dl
LEFT JOIN language l ON l.id = dl.language_id
WHERE dl.dictionary_id = d.id
ORDER BY dl.selection_order
) language_codes,
d.time_created,
d.time_modified,
d.time_content_modified,
d.description,
d.entries_have_domain_labels,
d.entries_have_label,
d.entries_have_definition,
d.entries_have_synonyms,
d.entries_have_links,
d.entries_have_other,
d.entries_have_foreign_languages,
d.entries_have_foreign_definitions,
d.entries_have_foreign_synonyms,
d.entries_have_images,
d.entries_have_audio,
d.entries_have_videos,
d.entries_have_terminology_review_flag,
d.entries_have_language_review_flag
FROM dictionary d
LEFT JOIN domain_primary dp ON dp.id = d.domain_primary_id
LEFT JOIN linked_dictionary ld on ld.target_dictionary_id = d.id
WHERE ld.id IS NULL AND d.status = 'published'
ORDER BY id
`
const { rows: dictionaryList } = await db.query(indexSql)
return dictionaryList
}
/**
* Fetch all published (complete) entries for given dictionary that were modified after since date.
* Entries shall include list of translations.
* @param dictionaryId
* @param since
* @returns {Promise<*>}
*/
static async getUpdatedEntriesSince(dictionaryId, since) {
const sqlEntries =
'SELECT id, term, label, definition, synonym FROM entry' +
" WHERE dictionary_id= $1 AND time_modified > $2 AND status='complete' AND is_published IS TRUE"
const sqlTranslations =
'SELECT l.code AS language_code, ef.entry_id, ef.term, ef.definition, ef.synonym FROM entry_foreign AS ef' +
' INNER JOIN language AS l ON l.id = ef.language_id' +
' WHERE entry_id = ANY($1::int[]) ORDER BY ef.entry_id, l.code'
const { rows: entryList } = await db.query(sqlEntries, [
dictionaryId,
since
])
const entryIds = entryList.map(e => {
return e.id
})
const { rows: translationList } = await db.query(sqlTranslations, [
entryIds
])
let currentEntryId = 0
let lastEntryId = 0
translationList.forEach(t => {
currentEntryId = t.entry_id
const entry = entryList.find(e => {
return e.id === currentEntryId
})
if (!entry) return
if (lastEntryId === 0) {
entry.translations = []
lastEntryId = t.entry_id
} else if (currentEntryId !== lastEntryId) {
// new entry translations : save previous
entry.translations = []
lastEntryId = currentEntryId
currentEntryId = t.entry_id
}
entry.translations.push(t)
})
return entryList
}
}
module.exports = InterInstanceSync
+486
View File
@@ -0,0 +1,486 @@
const db = require('./db')
const { aggregateSettings, deserialize } = require('./helpers/portal')
const Portal = {}
Portal.fetchInstanceSettings = async () => {
const text = `
SELECT
name, value
FROM
instance_settings
WHERE
name
IN (
'portal_name',
'portal_code',
'portal_description',
'is_consultancy_enabled',
'is_dictionaries_enabled',
'is_extraction_enabled')`
const { rows: fetchedSettings } = await db.query(text)
const aggregatedSettings = aggregateSettings(fetchedSettings)
const deserializedSettings = deserialize.settings(aggregatedSettings)
return deserializedSettings
}
Portal.updateInstaceSettings = async payload => {
const isExtractionEnabled = payload.isExtractionEnabled ? 'T' : 'F'
const isDictionariesEnabled = payload.isDictionariesEnabled ? 'T' : 'F'
const isConsultancyEnabled = payload.isConsultancyEnabled ? 'T' : 'F'
const values = [
payload.portalName,
payload.portalCode,
payload.portalDescription,
isExtractionEnabled,
isDictionariesEnabled,
isConsultancyEnabled
]
const text = `
UPDATE
instance_settings
SET
value
= CASE name
WHEN
'portal_name' THEN $1
WHEN
'portal_code' THEN $2
WHEN
'portal_description' THEN $3
WHEN
'is_extraction_enabled' THEN $4
WHEN
'is_dictionaries_enabled' THEN $5
WHEN
'is_consultancy_enabled' THEN $6
ELSE value
END`
await db.query(text, values)
}
Portal.fetchInstanceDictSettings = async () => {
const text = `
SELECT
name, value
FROM
instance_settings
WHERE
name
IN (
'min_entries_per_dictionary',
'dictionary_publish_approval',
'num_of_history_entires_per_entry',
'can_publish_entries_in_edit')`
const { rows: fetchedSettings } = await db.query(text)
const aggregatedSettings = aggregateSettings(fetchedSettings)
const deserializedSettings = deserialize.dictSettings(aggregatedSettings)
return deserializedSettings
}
Portal.updateInstaceDictSettings = async payload => {
const dictionaryPublish = payload.dictionaryPublishApproval ? 'T' : 'F'
const canPublishEntriesInEdit = payload.canPublishEntriesInEdit ? 'T' : 'F'
const values = [
payload.minEntriesPerDictionary,
dictionaryPublish,
// payload.keepNumOfExportsPerDict,
// payload.dictionaryAutoSaveFrequency,
payload.numOfHistoryEntriesPerEntry,
canPublishEntriesInEdit
]
const text = `
UPDATE
instance_settings
SET
value
= CASE name
WHEN
'min_entries_per_dictionary' THEN $1
WHEN
'dictionary_publish_approval' THEN $2
WHEN
'num_of_history_entires_per_entry' THEN $3
WHEN
'can_publish_entries_in_edit' THEN $4
ELSE value
END`
await db.query(text, values)
}
Portal.fetchInstanceConsultancySettings = async () => {
const text = `
SELECT
name, value
FROM
instance_settings
WHERE
name
IN (
'consultancy_type',
'zrc_email',
'zrc_url')`
const { rows: fetchedSettings } = await db.query(text)
const aggregatedSettings = aggregateSettings(fetchedSettings)
const deserializedSettings = deserialize.consultSettings(aggregatedSettings)
return deserializedSettings
}
Portal.updateInstaceConsultancySettings = async payload => {
const values = [payload.consultancyType, payload.zrcEmail, payload.zrcURL]
const text = `
UPDATE
instance_settings
SET
value
= CASE name
WHEN
'consultancy_type' THEN $1
WHEN
'zrc_email' THEN $2
WHEN
'zrc_url' THEN $3
ELSE value
END`
await db.query(text, values)
}
Portal.create = async portal => {
const isEnabled = true
const values = [
portal.name,
portal.url_update,
portal.url_index,
portal.code,
isEnabled
]
const text = `
INSERT INTO linked_portal (
name,
url_update,
url_index,
code,
is_enabled
)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`
const { rows } = await db.query(text, values)
return rows
}
Portal.fetchAll = async () => {
const text = `
SELECT
id,
name,
url_index,
is_enabled,
code,
time_last_synced,
EXISTS (SELECT ld.name
FROM linked_dictionary as ld
WHERE lp.id = ld.linked_portal_id) is_linked
FROM linked_portal as lp
ORDER BY id`
const { rows: fetchedConnections } = await db.query(text)
const deserializedConnections = fetchedConnections.map(connection =>
deserialize.connections(connection)
)
return deserializedConnections
}
Portal.fetchPortal = async portalId => {
const value = [portalId]
const text = `
SELECT
name,
url_update,
url_index,
code
FROM
linked_portal
WHERE
id = $1`
const { rows } = await db.query(text, value)
const fetchedPortal = rows[0]
const deserializedPortal = deserialize.connections(fetchedPortal)
return deserializedPortal
}
Portal.update = async (portalId, payload) => {
const values = [
payload.name,
payload.code,
payload.url_update,
payload.url_index,
portalId
]
const text = `
UPDATE
linked_portal
SET
name = $1,
code = $2,
url_update = $3,
url_index = $4
WHERE id = $5`
await db.query(text, values)
}
Portal.syncRemoteDictionaries = async (linkedPortalId, dictionaries) => {
const text = 'SELECT sync_remote_dictionaries($1, $2)'
const values = [linkedPortalId, dictionaries]
await db.query(text, values)
}
Portal.deleteLinkedDictionary = async linkedPortalId => {
const text = `DELETE from linked_portal WHERE id = $1`
const value = [linkedPortalId]
await db.query(text, value)
}
Portal.fetchDictionaries = async portalId => {
const value = [portalId]
const text = `
SELECT
name,
target_dictionary_id
FROM linked_dictionary
WHERE linked_portal_id = $1`
const { rows: fetchedDictionaries } = await db.query(text, value)
return fetchedDictionaries
}
Portal.fetchSelectedLinkedDictionaries = async (id, resultsPerPage, page) => {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $2::float)
FROM linked_dictionary
WHERE linked_dictionary.linked_portal_id = $1
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', linked_dictionary.id,
'name', linked_dictionary.name,
'isEnabled', linked_dictionary.is_enabled,
'code', linked_portal.code
)
FROM linked_dictionary
LEFT JOIN linked_portal
ON linked_dictionary.linked_portal_id = linked_portal.id
WHERE linked_dictionary.linked_portal_id = $1
ORDER BY linked_dictionary.id
LIMIT $2
OFFSET $3
)
) result`,
[id, resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
Portal.updateSelectedDictionaries = async (id, body) => {
const portalId = id
let enableArr = body.isEnabled ? Object.keys(body.isEnabled) : []
const dbClient = await db.getClient()
try {
await dbClient.query('BEGIN')
const setAllToFalse = `
UPDATE linked_dictionary
SET is_enabled = false
WHERE linked_portal_id = $1`
await dbClient.query(setAllToFalse, [portalId])
enableArr = enableArr.map(
linkedDictionaryId => +linkedDictionaryId.replaceAll("'", '')
)
const text = `
UPDATE linked_dictionary
SET is_enabled = true
WHERE id = ANY ($1)
AND linked_portal_id = $2`
const values = [enableArr, portalId]
await dbClient.query(text, values)
await dbClient.query('COMMIT')
} catch (error) {
await dbClient.query('ROLLBACK')
throw error
} finally {
dbClient.release()
}
}
Portal.fetchAllLinkedDictionaries = async (resultsPerPage, page) => {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $1::float)
FROM linked_dictionary
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', linked_dictionary.id,
'name', linked_dictionary.name,
'isEnabled', linked_dictionary.is_enabled,
'code', linked_portal.code
)
FROM linked_dictionary
LEFT JOIN linked_portal
ON linked_dictionary.linked_portal_id = linked_portal.id
ORDER BY linked_portal.id
LIMIT $1
OFFSET $2
)
) result`,
[resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
Portal.updateAllDictionaries = async body => {
const enableArr = Object.keys(body.isEnabled)
const dbClient = await db.getClient()
try {
await dbClient.query('BEGIN')
const setAllToFalse = `
UPDATE linked_dictionary
SET is_enabled = false`
await dbClient.query(setAllToFalse)
const enableAllDictionary = enableArr.map(async userId => {
userId = +userId.replaceAll("'", '')
const text = `
UPDATE linked_dictionary
SET is_enabled = true
WHERE id = $1`
const values = [userId]
await dbClient.query(text, values)
})
await Promise.all(enableAllDictionary)
await dbClient.query('COMMIT')
} catch (error) {
await dbClient.query('ROLLBACK')
throw error
} finally {
dbClient.release()
}
}
Portal.updatePortalStatus = async (portalId, isEnabled) => {
const values = [isEnabled, portalId]
const text = `
UPDATE linked_portal
SET is_enabled = $1
WHERE id = $2`
await db.query(text, values)
}
Portal.getInstanceSettingValue = async settingName => {
const values = [settingName]
const text = `
SELECT value
FROM instance_settings
WHERE name = $1`
const { rows } = await db.query(text, values)
const settingValue = rows[0].value
return settingValue
}
Portal.fetchAllInstanceSettingNames = async () => {
const { rows } = await db.query('SELECT name FROM instance_settings')
const settingNames = rows.map(row => row.name)
return settingNames
}
Portal.getSlovenianLanguageId = async () => {
const { rows } = await db.query("SELECT id FROM language WHERE code = 'sl'")
const id = rows[0].id
return id
}
Portal.getSearchAggregateNames = async (
primaryDomainIds,
dictionaryIds,
languageIds
) => {
const text = `
SELECT jsonb_build_object(
'primaryDomains', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
FROM domain_primary
WHERE id = ANY ($1)
)
),
'dictionaries', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
FROM dictionary
WHERE id = ANY ($2)
)
),
'languages', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
FROM language
WHERE id = ANY ($3)
)
)
) "names"
`
const values = [primaryDomainIds, dictionaryIds, languageIds]
const {
rows: [{ names }]
} = await db.query(text, values)
return names
}
module.exports = Portal
+244
View File
@@ -0,0 +1,244 @@
const debug = require('debug')('termPortal:models/search-engine')
const { Client } = require('@opensearch-project/opensearch')
const ENTRY_INDEX = 'entry'
const CONSULTANCY_ENTRY_INDEX = 'consultancy_entry'
const client = new Client({ node: 'http://opensearch:9200' })
exports.initEntryIndex = async () => {
const { statusCode } = await client.indices.exists({ index: ENTRY_INDEX })
const doesIndexExist = statusCode === 200
if (doesIndexExist) {
debug('Entry search index already exists.')
return
}
await client.indices.create({
index: ENTRY_INDEX,
body: {
settings: {
number_of_replicas: 0
},
mappings: {
dynamic: 'strict',
properties: {
id: { type: 'keyword', index: false, doc_values: false },
isValid: { type: 'boolean', doc_values: false },
isPublished: { type: 'boolean', doc_values: false },
isTerminologyReviewed: { type: 'boolean', doc_values: false },
isLanguageReviewed: { type: 'boolean', doc_values: false },
status: { type: 'keyword', doc_values: false },
term: {
type: 'text',
doc_values: false,
fields: {
keyword: { type: 'keyword', doc_values: false },
sort: {
type: 'icu_collation_keyword',
index: false,
language: 'sl',
country: 'SI'
}
}
},
homonymSort: { type: 'keyword', index: false },
label: { type: 'text', doc_values: false },
definition: { type: 'text', doc_values: false },
synonyms: { type: 'text', doc_values: false },
other: { type: 'text', doc_values: false },
timeMostRecentComment: { type: 'date' },
domainLabels: { type: 'text', doc_values: false },
links: { type: 'text', doc_values: false },
foreignEntries: {
type: 'nested',
properties: {
lang: {
properties: {
id: { type: 'keyword' },
code: { type: 'keyword', index: false, doc_values: false },
nameSl: { type: 'keyword', index: false, doc_values: false },
nameEn: { type: 'keyword', index: false, doc_values: false }
}
},
terms: {
type: 'text',
doc_values: false,
fields: {
keyword: { type: 'keyword', doc_values: false }
}
},
definition: { type: 'text', doc_values: false },
synonyms: { type: 'text', doc_values: false }
}
},
primaryDomain: {
properties: {
id: { type: 'keyword' },
nameSl: { type: 'keyword', index: false, doc_values: false },
nameEn: { type: 'keyword', index: false, doc_values: false }
}
},
dictionary: {
properties: {
id: { type: 'keyword' },
nameSl: { type: 'keyword', index: false, doc_values: false },
nameSlShort: { type: 'keyword', index: false, doc_values: false },
nameEn: { type: 'keyword', index: false, doc_values: false },
status: { type: 'keyword', doc_values: false }
}
},
source: {
properties: {
code: { type: 'keyword' },
name: { type: 'keyword', index: false, doc_values: false }
}
}
}
}
}
})
debug('Entry search index created.')
}
exports.initConsultancyEntryIndex = async () => {
const { statusCode } = await client.indices.exists({
index: CONSULTANCY_ENTRY_INDEX
})
const doesIndexExist = statusCode === 200
if (doesIndexExist) {
debug('Consultancy entry search index already exists.')
return
}
await client.indices.create({
index: CONSULTANCY_ENTRY_INDEX,
body: {
settings: {
number_of_replicas: 0
},
mappings: {
dynamic: 'strict',
properties: {
id: { type: 'keyword', index: false, doc_values: false },
timeCreated: { type: 'date', index: false },
status: { type: 'keyword', doc_values: false },
description: { type: 'text', index: false, doc_values: false },
title: { type: 'text', doc_values: false },
question: { type: 'text', doc_values: false },
answer: { type: 'text', doc_values: false },
answerAuthors: { type: 'keyword', index: false, doc_values: false },
primaryDomain: {
properties: {
id: { type: 'keyword', doc_values: false },
nameSl: { type: 'keyword', index: false, doc_values: false },
nameEn: { type: 'keyword', index: false, doc_values: false }
}
},
assignedConsultants: {
properties: {
id: { type: 'keyword', doc_values: false },
firstName: { type: 'keyword', index: false, doc_values: false },
lastName: { type: 'keyword', index: false, doc_values: false },
isModerator: { type: 'boolean', index: false, doc_values: false }
}
}
}
}
}
})
debug('Consultancy entry search index created.')
}
exports.waitForConnection = () => {
return new Promise(resolve => {
async function testConnection() {
try {
// eslint-disable-next-line no-console
console.log('Verifying connection to search engine server')
await client.ping()
// eslint-disable-next-line no-console
console.log('Connection to search engine server verified')
resolve()
} catch {
// eslint-disable-next-line no-console
console.error('Could not connect to search engine server')
setTimeout(testConnection, 1000)
}
}
testConnection()
})
}
exports.searchEngineClient = client
exports.ENTRY_INDEX = ENTRY_INDEX
exports.CONSULTANCY_ENTRY_INDEX = CONSULTANCY_ENTRY_INDEX
// A wrapper, to simplify querying the entry index.
exports.searchEntryIndex = async query => {
const hits = await client.search({
index: ENTRY_INDEX,
filter_path: 'hits,aggregations',
body: query
})
return hits
}
// A wrapper, to simplify querying the consultancy entry index.
exports.searchConsultancyEntryIndex = async query => {
const hits = await client.search({
index: CONSULTANCY_ENTRY_INDEX,
filter_path: 'hits',
body: query
})
return hits
}
exports.deleteEntryFromIndex = async (entryId, shouldWait) => {
await client.delete({
index: ENTRY_INDEX,
id: entryId,
refresh: shouldWait ? 'wait_for' : false
})
}
exports.deleteConsultancyEntryFromIndex = async (entryId, shouldWait) => {
await client.delete({
index: CONSULTANCY_ENTRY_INDEX,
id: entryId,
refresh: shouldWait ? 'wait_for' : false
})
}
exports.deleteDictionaryEntriesFromIndex = async dictionaryId => {
await client.deleteByQuery({
index: ENTRY_INDEX,
body: {
query: {
term: {
'dictionary.id': {
value: dictionaryId
}
}
}
}
})
}
exports.deleteConsultancyEntriesFromIndex = async () => {
await client.deleteByQuery({
index: CONSULTANCY_ENTRY_INDEX,
body: {
query: {
match_all: {}
}
}
})
}
+183
View File
@@ -0,0 +1,183 @@
const db = require('../../models/db')
const Cursor = require('pg-cursor')
const axios = require('axios').default
const xmlFlow = require('xml-flow')
const { removeHtmlTags } = require('../helpers')
const Eurotermbank = {}
// Push all changes to Eurotermbank.
Eurotermbank.push = async () => {
const dbClient = await db.getClient()
try {
const { rows: dictrionariesToSync } = await dbClient.query(
'SELECT id, name_sl FROM dictionary ORDER BY id'
)
for (const { id, name_sl: name } of dictrionariesToSync) {
// Create/update collection metadata.
await centralSyncApi.put(id.toString(), {
name,
domainid: 2841
})
const text = `
SELECT
id,
term,
definition,
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
) domain_labels,
ARRAY(
SELECT jsonb_build_object(
'link', link,
'type', type)
FROM entry_link
WHERE entry_id = e.id
) links,
ARRAY(
SELECT jsonb_build_object(
'lang_code', l.code,
'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
) foreign_entries
FROM entry e
WHERE
e.dictionary_id = $1
AND is_valid`
const values = [id]
const cursor = dbClient.query(new Cursor(text, values))
let entries = []
do {
// Keep getting and sending entries in batches of 100.
entries = await cursor.read(100)
if (entries.length) {
const tbxPayload = generateTbxPayload(entries)
await centralSyncApi.post(`${id}/entries`, tbxPayload, {
headers: { 'content-type': 'application/xml' }
})
}
} while (entries.length === 100)
}
} finally {
dbClient.release()
}
}
const centralSyncApi = axios.create({
baseURL:
'https://test-fedterm.eurotermbank.com/api/termservice/sync/collection/external/',
auth: {
username: 'SlovenianNTP',
password: '3l063Ni=p0tr4l(t3rm'
}
})
function generateTbxPayload(entries) {
// Opening boilerplate.
let tbxPayload =
'<?xml version="1.0" encoding="utf-8"?><!DOCTYPE martif SYSTEM "https://eurotermbank.com/TBXcoreStructV02%20%281%29.dtd"><martif type="TBX" xml:lang="en"><martifHeader><fileDesc><sourceDesc><p>Sync collection sample</p></sourceDesc></fileDesc><encodingDesc><p type="XCSURI">https://eurotermbank.com/tbx-0.5.1.xcs</p></encodingDesc></martifHeader><text><body>'
// Entry content TBX.
tbxPayload += entries.reduce(
(payload, entry) => (payload += generateEntryTbx(entry)),
''
)
// Closing boilerplate.
tbxPayload += '</body></text></martif>'
return tbxPayload
}
function generateEntryTbx(entry) {
const entryObj = {
$name: 'termEntry',
$attrs: { id: entry.id },
$markup: [
{
$name: 'langSet',
$attrs: { 'xml:lang': 'sl' },
$markup: [
{
$name: 'ntig',
$markup: [
{
$name: 'termGrp',
$markup: [{ $name: 'term', $text: removeHtmlTags(entry.term) }]
}
]
}
]
}
]
}
if (entry.definition) {
const definitionObj = {
$name: 'descripGrp',
$markup: [
{
$name: 'descrip',
$attrs: { type: 'definition' },
$text: removeHtmlTags(entry.definition)
}
]
}
entryObj.$markup[0].$markup.push(definitionObj)
}
entry.foreign_entries.forEach(fEntry => {
const fEntryObj = {
$name: 'langSet',
$attrs: { 'xml:lang': fEntry.lang_code },
$markup: [
{
$name: 'ntig',
$markup: [
{
$name: 'termGrp',
$markup: [
{ $name: 'term', $text: removeHtmlTags(fEntry.terms[0]) }
]
}
]
}
]
}
if (fEntry.definition) {
const fDefinitionObj = {
$name: 'descripGrp',
$markup: [
{
$name: 'descrip',
$attrs: { type: 'definition' },
$text: removeHtmlTags(fEntry.definition)
}
]
}
fEntryObj.$markup.push(fDefinitionObj)
}
entryObj.$markup.push(fEntryObj)
})
return xmlFlow.toXml(entryObj)
}
module.exports = Eurotermbank
+470
View File
@@ -0,0 +1,470 @@
const db = require('./db')
const bcrypt = require('bcrypt')
const uid = require('uid-safe')
const { deserialize } = require('./helpers/user')
const User = {}
// Create new user in DB.
User.create = async user => {
const SALT_ROUNDS = 12
const bcryptHash = await bcrypt.hash(user.password, SALT_ROUNDS)
const values = [
user.username || null,
user.firstName || null,
user.lastName || null,
user.email || null,
bcryptHash || null
]
const text = `INSERT INTO "user" (
username,
first_name,
last_name,
email,
bcrypt_hash
)
VALUES (${db.genParamStr(values)})
RETURNING id`
const { rows } = await db.query(text, values)
const userId = rows[0].id
return userId
}
// Save an activation token for a single user in DB.
User.saveActivationToken = async (userId, activationToken) => {
await db.query(
'INSERT INTO user_token_activation (token, user_id) VALUES ($1, $2)',
[activationToken, userId]
)
}
// Fetch user from DB by (valid) activation token.
User.fetchByActivationToken = async activationToken => {
const TOKEN_VALID_PERIOD = '1 week'
const text = `
SELECT u.id
FROM user_token_activation t
INNER JOIN "user" u ON u.id = t.user_id
WHERE
t.token = $1
AND AGE(NOW(), t.time_created) < INTERVAL '${TOKEN_VALID_PERIOD}'
`
const values = [activationToken]
const { rows } = await db.query(text, values)
const user = rows[0]
// TODO Perhaps suggest to the user to request another one and make a shortcut.
if (!user) throw Error('Povezava je neveljavna ali pa je že potekla')
return user
}
// Activate user account.
User.activateAccount = async user => {
await db.query(`UPDATE "user" SET status = 'active' WHERE id = $1`, [user.id])
}
// Generate a user remember me token.
User.generateRememberMeToken = async () => {
const token = await uid(32)
return token
}
// Save a remember me token for a single user in DB.
User.saveRememberMeToken = async (user, rememberMeToken) => {
await db.query(
'INSERT INTO user_token_remember_me (token, user_id) VALUES ($1, $2)',
[rememberMeToken, user.id]
)
}
// Remove a specific remember me token from DB.
User.clearRememberMeToken = async rememberMeToken => {
await db.query('DELETE FROM user_token_remember_me WHERE token = $1', [
rememberMeToken
])
}
// Fetch user data that should be available on every request from DB by id.
User.fetchDeserializedDataById = async userId => {
const text = `
SELECT
u.id,
u.username,
u.first_name,
u.last_name,
u.email,
u.hits_per_page,
ARRAY(
SELECT jsonb_build_object(
'roleName', r.role_name,
'dictionaryId', r.dictionary_id,
'administration', r.administration,
'terminologyReview', r.terminology_review,
'languageReview', r.language_review,
'editing', r.editing)
FROM user_role r
WHERE r.user_id = u.id
) user_roles,
ARRAY(
SELECT jsonb_build_object(
'id', cr.entry_id,
'isModerator', cr.is_moderator)
FROM consultancy_entry_consultant cr
WHERE cr.user_id = u.id
) assigned_consultancy_entries
FROM "user" u
WHERE
u.id = $1`
const values = [userId]
const {
rows: [user]
} = await db.query(text, values)
const deserializedUser = deserialize.userById(user)
return deserializedUser
}
// Fetch all registered users on the portal
User.fetchAll = async (resultsPerPage, page) => {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $1::float)
FROM "user"
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', id,
'userName', username,
'email', email,
'status', status
)
FROM "user"
ORDER BY username
LIMIT $1
OFFSET $2
)
) result
`,
[resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
// Fetch all users with at least one portal role along with all their portal roles.
User.fetchAllWithPortalRoles = async () => {
const text = `
SELECT u.id,
u.username,
u.email,
jsonb_build_object(
'isPortalAdmin', 'portal admin' = ANY (r.roles_array),
'isDictionariesAdmin', 'dictionaries admin' = ANY (r.roles_array),
'isConsultancyAdmin', 'consultancy admin' = ANY (r.roles_array)
) roles
FROM (
SELECT user_id, array_agg(role_name) roles_array
FROM user_role
WHERE role_name IN ('portal admin', 'dictionaries admin', 'consultancy admin')
GROUP BY user_id
) r
LEFT JOIN "user" u ON u.id = r.user_id
ORDER BY u.id
`
const { rows: users } = await db.query(text)
return users
}
// Find searched user with specific username or email
User.findByUsernameOrEmail = async userNameEmail => {
const text = `
SELECT
id,
username,
email
FROM "user"
WHERE username=$1
OR email=$1`
const value = [userNameEmail]
const { rows: user } = await db.query(text, value)
return user
}
// Delete relevant portal roles and set new ones.
User.updatePortalRoles = async rolesPerUser => {
const rolesPerUserArr = Object.entries(rolesPerUser)
const dbClient = await db.getClient()
try {
await dbClient.query('BEGIN')
const removeAllPortalRolesQuery = `
DELETE FROM user_role
WHERE role_name IN ('portal admin', 'dictionaries admin', 'consultancy admin')`
await dbClient.query(removeAllPortalRolesQuery)
const updateRoles = rolesPerUserArr.map(async ([userId, roles]) => {
userId = +userId.replaceAll("'", '')
const rolesArr = []
if (roles.isPortalAdmin) rolesArr.push('portal admin')
if (roles.isDictionariesAdmin) rolesArr.push('dictionaries admin')
if (roles.isConsultancyAdmin) {
rolesArr.push('consultancy admin')
rolesArr.push('consultant')
}
const updateRolesPerUser = rolesArr.map(async roleName => {
const text = `
INSERT INTO user_role (user_id, role_name)
VALUES ($1, $2)`
const values = [userId, roleName]
await dbClient.query(text, values)
})
await Promise.all(updateRolesPerUser)
})
await Promise.all(updateRoles)
const countAdminsQuery = `
SELECT COUNT(*) portal_admins_count
FROM user_role
WHERE role_name = 'portal admin'`
const { rows } = await dbClient.query(countAdminsQuery)
const portalAdminsCount = +rows[0].portal_admins_count
if (!portalAdminsCount) throw Error('Can not delete all portal admins')
await dbClient.query('COMMIT')
} catch (error) {
await dbClient.query('ROLLBACK')
throw error
} finally {
dbClient.release()
}
}
User.fetchUser = async userId => {
const text = `
SELECT id, username, first_name, last_name, email
FROM "user"
WHERE id=$1`
const value = [userId]
const { rows } = await db.query(text, value)
const fetchedUser = rows[0]
const deserializedUserData = deserialize.user(fetchedUser)
return deserializedUserData
}
User.updateUser = async (userId, payload) => {
const text = `
UPDATE "user"
SET
username = $2,
first_name = $3,
last_name = $4
WHERE id = $1`
const values = [userId, payload.username, payload.firstName, payload.lastName]
await db.query(text, values)
}
User.fetchUserRoles = async userId => {
const text = `
SELECT
jsonb_build_object(
'isPortalAdmin', 'portal admin' = ANY (r.roles_array),
'isDictionariesAdmin', 'dictionaries admin' = ANY (r.roles_array),
'isConsultancyAdmin', 'consultancy admin' = ANY (r.roles_array),
'isConsultant', 'consultant' = ANY (r.roles_array),
'isEditor', 'editor' = ANY (r.roles_array)
) roles
FROM (
SELECT user_id, array_agg(role_name) roles_array
FROM user_role
WHERE role_name IN ('portal admin', 'dictionaries admin', 'consultancy admin', 'consultant', 'editor')
GROUP BY user_id
) r
LEFT JOIN "user" u ON u.id = r.user_id
WHERE u.id=$1`
const value = [userId]
const { rows } = await db.query(text, value)
const fetchedUser = rows[0]
return fetchedUser
}
User.fetchAllWithDictionaryRights = async dictionaryId => {
const text = `
SELECT u.username,u.email, u.id, r.administration, r.editing, r.terminology_review, r.language_review
FROM user_role r
INNER JOIN "user" u ON u.id = r.user_id
WHERE dictionary_id = $1
ORDER BY u.username`
const value = [dictionaryId]
const { rows: fetchedUserRights } = await db.query(text, value)
const deserializedRights = fetchedUserRights.map(user =>
deserialize.userRights(user)
)
return deserializedRights
}
User.updateUserRights = async (dictionaryId, rightsPerUser) => {
const rightsPerUserArr = Object.entries(rightsPerUser)
const dbClient = await db.getClient()
try {
await dbClient.query('BEGIN')
const value = [dictionaryId]
const text = `
DELETE FROM user_role
WHERE dictionary_id = $1`
await dbClient.query(text, value)
const roleName = 'editor'
const updateRights = rightsPerUserArr.map(async ([userId, roles]) => {
userId = +userId.replaceAll("'", '')
const values = [
userId,
roleName,
dictionaryId,
!!roles.isAdministration,
!!roles.isEditing,
!!roles.isTerminologyReview,
!!roles.isLanguageReview
]
const text = `
INSERT INTO user_role (user_id, role_name, dictionary_id, administration, editing, terminology_review, language_review)
VALUES ($1, $2, $3, $4, $5, $6, $7)`
await dbClient.query(text, values)
})
await Promise.all(updateRights)
const textCount = `
SELECT COUNT(*) dictionary_admins_count
FROM user_role
WHERE administration = true
AND dictionary_id = $1`
const { rows } = await dbClient.query(textCount, value)
const portalAdminsCount = +rows[0].dictionary_admins_count
if (!portalAdminsCount) throw Error('Can not delete all dictionary admins')
await dbClient.query('COMMIT')
} catch (error) {
await dbClient.query('ROLLBACK')
throw error
} finally {
dbClient.release()
}
}
User.deleteUserDictionary = async dictionaryId => {
const text = 'DELETE FROM user_role WHERE dictionary_id = $1'
const value = [dictionaryId]
await db.query(text, value)
}
// Fetch user with role consultant or consultancy admin
User.fetchConsultants = async () => {
// fetch domains string for the user
const text = `
SELECT DISTINCT u.id, u.username, u.first_name, u.last_name, ur.domains
FROM user_role ur
INNER JOIN "user" u ON u.id = ur.user_id
ORDER BY u.id ASC
`
const { rows } = await db.query(text)
const users = rows
return users
}
// Insert consultancy role domains
User.updateConsultancyDomains = async (id, value) => {
await db.query('UPDATE user_role SET domains=$1 WHERE user_id = $2', [
value,
id
])
}
// Insert new consultant role with domain of
User.insertNewConsultantWithDomain = async (userId, domains) => {
const { rows } = await db.query(
"SELECT user_id FROM user_role WHERE user_id = $1 and role_name = 'consultant'",
[userId]
)
if (rows.length) return
await db.query(
"INSERT INTO user_role (user_id, domains, role_name) VALUES ($1, $2, 'consultant')",
[userId, domains]
)
}
// Insert new consultant role with domain of
User.insertNewConsultantWithDomainByUsername = async (username, domains) => {
const { rows } = await db.query(
'SELECT id FROM "user" WHERE username = $1 or email = $1',
[username]
)
await User.insertNewConsultantWithDomain(rows[0].id, domains)
}
// Remove consultant role
User.removeConsultant = async userId => {
await db.query(
`DELETE FROM user_role
WHERE user_id=$1 and role_name='consultant'`,
[userId]
)
}
User.fetchAllowedHitsPerPage = async () => {
return (
await db.query(`SELECT unnest(enum_range(NULL::user_hits_per_page))`)
).rows.map(e => e.unnest)
}
User.updateFirstNameAndLastName = async (username, firstName, lastName) => {
return await db.query(
`UPDATE "user"
SET first_name=$2, last_name=$3
WHERE username=$1;`,
[username, firstName, lastName]
)
}
User.updateHitsPerPage = async (username, hitsPerPageAmount) => {
return await db.query(
`UPDATE "user"
SET hits_per_page=$2::user_hits_per_page
WHERE username=$1;`,
[username, hitsPerPageAmount]
)
}
module.exports = User