Initial commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user