Files
libredesk/internal/helpcenter/queries.sql
Abhinav Raut 170fcd026d add help article citations to AI replies
Link cited public help articles in chat replies and emails. Batch reference lookups and keep source metadata out of customer replies.
2026-10-10 20:57:12 +05:30

553 lines
20 KiB
SQL

-- name: get-all-help-centers
SELECT id, created_at, updated_at, name, slug, page_title, meta_description, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, custom_domain, template, livechat_inbox_id
FROM help_centers
ORDER BY created_at DESC;
-- name: get-active-help-centers
SELECT id, created_at, updated_at, name, slug, page_title, meta_description, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, custom_domain, template, livechat_inbox_id
FROM help_centers
WHERE is_active = true
ORDER BY created_at DESC;
-- name: get-help-center-by-id
SELECT id, created_at, updated_at, name, slug, page_title, meta_description, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, custom_domain, template, livechat_inbox_id
FROM help_centers
WHERE id = $1;
-- name: get-help-center-by-slug
SELECT id, created_at, updated_at, name, slug, page_title, meta_description, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, custom_domain, template, livechat_inbox_id
FROM help_centers
WHERE slug = $1 AND is_active = true;
-- name: insert-help-center
INSERT INTO help_centers (name, slug, page_title, meta_description, custom_css, custom_js, default_locale, allowed_locales, theme, custom_domain, template, livechat_inbox_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *;
-- name: update-help-center
UPDATE help_centers
SET name = $2, slug = $3, page_title = $4, meta_description = $5, custom_css = $6, custom_js = $7, default_locale = $8, allowed_locales = $9, theme = $10, custom_domain = $11, template = $12, livechat_inbox_id = $13, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: toggle-help-center-active
UPDATE help_centers
SET is_active = NOT is_active, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: delete-help-center
DELETE FROM help_centers
WHERE id = $1;
-- name: get-help-center-locales-in-use
SELECT DISTINCT locale FROM article_collections WHERE help_center_id = $1
UNION
SELECT DISTINCT a.locale
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
WHERE c.help_center_id = $1;
-- name: get-collections-by-help-center
SELECT id, created_at, updated_at, help_center_id, slug, parent_id, locale, name, description, icon, sort_order, is_published
FROM article_collections
WHERE help_center_id = $1
ORDER BY sort_order ASC, created_at DESC;
-- name: get-collection-by-id
SELECT id, created_at, updated_at, help_center_id, slug, parent_id, locale, name, description, icon, sort_order, is_published
FROM article_collections
WHERE id = $1;
-- name: get-collection-subtree-depth
WITH RECURSIVE subtree AS (
SELECT id, 1 AS depth FROM article_collections WHERE id = $1
UNION ALL
SELECT c.id, s.depth + 1 FROM article_collections c JOIN subtree s ON c.parent_id = s.id
)
SELECT COALESCE(MAX(depth), 1) FROM subtree;
-- name: lock-help-center
SELECT id FROM help_centers WHERE id = $1 FOR UPDATE;
-- name: lock-help-center-by-collection
SELECT hc.id FROM help_centers hc
JOIN article_collections c ON c.help_center_id = hc.id
WHERE c.id = $1
FOR UPDATE OF hc;
-- name: collection-has-content
SELECT EXISTS(SELECT 1 FROM article_collections WHERE parent_id = $1)
OR EXISTS(SELECT 1 FROM help_articles WHERE collection_id = $1);
-- name: get-article-ids-in-collection-subtree
WITH RECURSIVE subtree AS (
SELECT id FROM article_collections WHERE id = $1
UNION ALL
SELECT c.id FROM article_collections c JOIN subtree s ON c.parent_id = s.id
)
SELECT a.id FROM help_articles a WHERE a.collection_id IN (SELECT id FROM subtree);
-- name: get-article-ids-in-help-center
SELECT a.id FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
WHERE c.help_center_id = $1;
-- name: update-collection-sort-order
UPDATE article_collections
SET sort_order = $3, updated_at = NOW()
WHERE id = $1 AND help_center_id = $2;
-- name: get-collection-by-id-for-update
SELECT id, created_at, updated_at, help_center_id, slug, parent_id, locale, name, description, icon, sort_order, is_published
FROM article_collections
WHERE id = $1
FOR UPDATE;
-- name: collection-slug-exists-in-help-center
SELECT EXISTS(
SELECT 1
FROM article_collections
WHERE help_center_id = $1 AND slug = $2 AND locale = $3
);
-- name: insert-collection
INSERT INTO article_collections (help_center_id, slug, parent_id, locale, name, description, icon, sort_order, is_published)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *;
-- name: update-collection
UPDATE article_collections
SET slug = $2, parent_id = $3, locale = $4, name = $5, description = $6, icon = $7, sort_order = $8, is_published = $9, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: toggle-collection-published
UPDATE article_collections
SET is_published = NOT is_published, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: delete-collection
DELETE FROM article_collections
WHERE id = $1;
-- name: get-article-by-id
SELECT a.id, a.created_at, a.updated_at, a.collection_id, a.author_id, a.created_by, a.translation_group_id, a.slug, a.locale, a.title, a.content,
a.excerpt, a.meta_title, a.meta_description, a.meta_image_url, a.sort_order, a.status, a.view_count, a.ai_enabled,
TRIM(u.first_name || ' ' || COALESCE(u.last_name, '')) AS author_name,
TRIM(cu.first_name || ' ' || COALESCE(cu.last_name, '')) AS created_by_name,
(SELECT COUNT(*) FROM help_article_feedback f WHERE f.article_id = a.id AND f.is_helpful) AS helpful_count,
(SELECT COUNT(*) FROM help_article_feedback f WHERE f.article_id = a.id AND NOT f.is_helpful) AS not_helpful_count
FROM help_articles a
LEFT JOIN users u ON u.id = a.author_id
LEFT JOIN users cu ON cu.id = a.created_by
WHERE a.id = $1;
-- name: get-article-translations
SELECT a.id, a.collection_id, a.locale, a.slug, a.title, a.status
FROM help_articles a
WHERE a.translation_group_id = (SELECT translation_group_id FROM help_articles WHERE id = $1)
ORDER BY a.locale;
-- name: insert-article
INSERT INTO help_articles (collection_id, author_id, created_by, slug, locale, title, content, excerpt, meta_title, meta_description, meta_image_url, sort_order, status, ai_enabled, translation_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, COALESCE(NULLIF($15, '')::UUID, gen_random_uuid()))
RETURNING id, created_at, updated_at, collection_id, author_id, created_by,
translation_group_id, slug, locale, title, content, excerpt, meta_title,
meta_description, meta_image_url, sort_order, status, view_count,
ai_enabled, embedded_fingerprint, search_tsv;
-- name: update-article
UPDATE help_articles
SET collection_id = COALESCE($9, collection_id), slug = $2, locale = $3, title = $4, content = $5, sort_order = $6, status = $7, ai_enabled = $8,
excerpt = $10, meta_title = $11, meta_description = $12, meta_image_url = $13, author_id = $14, updated_at = NOW()
WHERE id = $1
RETURNING id, created_at, updated_at, collection_id, author_id, created_by,
translation_group_id, slug, locale, title, content, excerpt, meta_title,
meta_description, meta_image_url, sort_order, status, view_count,
ai_enabled, embedded_fingerprint, search_tsv;
-- name: user-is-author-assignable
SELECT EXISTS(
SELECT 1 FROM users
WHERE id = $1 AND type IN ('agent', 'ai_assistant') AND deleted_at IS NULL
);
-- name: article-slug-exists-in-help-center
SELECT EXISTS(
SELECT 1
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
WHERE c.help_center_id = (SELECT help_center_id FROM article_collections WHERE id = $1)
AND a.slug = $2 AND a.locale = $3
);
-- name: other-article-slug-exists-in-help-center
SELECT EXISTS(
SELECT 1
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
WHERE c.help_center_id = (SELECT help_center_id FROM article_collections WHERE id = $1)
AND a.slug = $2 AND a.locale = $3 AND a.id <> $4
);
-- name: move-article-to-collection
UPDATE help_articles
SET collection_id = $2, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: update-article-sort-order
UPDATE help_articles
SET sort_order = $3, updated_at = NOW()
WHERE id = $1 AND collection_id = $2;
-- name: update-article-status
UPDATE help_articles
SET status = $2, updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: get-linkable-translation-articles
SELECT a.id, a.title, a.locale, c.name AS collection_name
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
WHERE c.help_center_id = $1
AND a.locale != $2
AND NOT EXISTS (
SELECT 1 FROM help_articles sibling
WHERE sibling.translation_group_id = a.translation_group_id AND sibling.id != a.id
)
ORDER BY a.locale, a.title;
-- name: article-locale-in-translation-group
SELECT EXISTS(
SELECT 1 FROM help_articles
WHERE translation_group_id = $1 AND locale = $2
);
-- name: count-article-translation-siblings
SELECT COUNT(*) FROM help_articles
WHERE translation_group_id = $1 AND id != $2;
-- name: link-article-translation
UPDATE help_articles a
SET translation_group_id = $2::UUID, updated_at = NOW()
WHERE a.id = $1
AND NOT EXISTS (
SELECT 1 FROM help_articles sibling
WHERE sibling.translation_group_id = a.translation_group_id AND sibling.id != a.id
)
RETURNING *;
-- name: unlink-article-translation
UPDATE help_articles
SET translation_group_id = gen_random_uuid(), updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: delete-article
DELETE FROM help_articles
WHERE id = $1;
-- name: get-help-center-tree-data
SELECT
'collection' AS type,
c.id,
c.created_at,
c.updated_at,
c.help_center_id,
c.slug,
c.parent_id,
c.locale,
c.name,
c.description,
c.icon,
c.sort_order,
c.is_published,
NULL::INTEGER AS collection_id,
NULL::TEXT AS title,
NULL::TEXT AS content,
NULL::TEXT AS status,
NULL::INTEGER AS view_count,
NULL::BOOLEAN AS ai_enabled,
NULL::TEXT AS author_name,
NULL::TEXT AS author_avatar
FROM article_collections c
WHERE c.help_center_id = $1 AND ($2 = '' OR c.locale = $2)
UNION ALL
SELECT
'article' AS type,
a.id,
a.created_at,
a.updated_at,
c.help_center_id,
a.slug,
NULL::INTEGER AS parent_id,
a.locale,
a.title AS name,
NULL::TEXT AS description,
NULL::TEXT AS icon,
a.sort_order,
NULL::BOOLEAN AS is_published,
a.collection_id,
a.title,
'' AS content,
a.status,
a.view_count,
a.ai_enabled,
NULL::TEXT AS author_name,
NULL::TEXT AS author_avatar
FROM help_articles a
JOIN article_collections c ON a.collection_id = c.id
WHERE c.help_center_id = $1 AND ($2 = '' OR a.locale = $2)
ORDER BY type DESC, parent_id NULLS FIRST, sort_order, name;
-- name: get-public-tree-data
WITH RECURSIVE published_collections AS (
SELECT id FROM article_collections
WHERE help_center_id = $1 AND parent_id IS NULL AND is_published = true AND ($2 = '' OR locale = $2)
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true AND ($2 = '' OR c.locale = $2)
)
SELECT
'collection' AS type,
c.id,
c.created_at,
c.updated_at,
c.help_center_id,
c.slug,
c.parent_id,
c.locale,
c.name,
c.description,
c.icon,
c.sort_order,
c.is_published,
NULL::INTEGER AS collection_id,
NULL::TEXT AS title,
NULL::TEXT AS content,
NULL::TEXT AS status,
NULL::INTEGER AS view_count,
NULL::BOOLEAN AS ai_enabled,
NULL::TEXT AS author_name,
NULL::TEXT AS author_avatar
FROM article_collections c
WHERE c.id IN (SELECT id FROM published_collections)
UNION ALL
SELECT
'article' AS type,
a.id,
a.created_at,
a.updated_at,
c.help_center_id,
a.slug,
NULL::INTEGER AS parent_id,
a.locale,
a.title AS name,
NULL::TEXT AS description,
NULL::TEXT AS icon,
a.sort_order,
NULL::BOOLEAN AS is_published,
a.collection_id,
a.title,
'' AS content,
a.status,
a.view_count,
a.ai_enabled,
TRIM(u.first_name || ' ' || COALESCE(u.last_name, '')) AS author_name,
u.avatar_url AS author_avatar
FROM help_articles a
JOIN article_collections c ON a.collection_id = c.id AND c.locale = a.locale
LEFT JOIN users u ON u.id = a.author_id
WHERE c.id IN (SELECT id FROM published_collections) AND a.status = 'published'
AND ($2 = '' OR a.locale = $2)
ORDER BY type DESC, parent_id NULLS FIRST, sort_order, name;
-- name: get-published-article-by-slug
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
SELECT a.id, a.created_at, a.updated_at, a.collection_id, a.author_id, a.slug, a.locale, a.title, a.content,
a.excerpt, a.meta_title, a.meta_description, a.meta_image_url, a.sort_order, a.status, a.view_count, a.ai_enabled,
TRIM(u.first_name || ' ' || COALESCE(u.last_name, '')) AS author_name,
u.avatar_url AS author_avatar
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale AND c.id IN (SELECT id FROM published_collections)
LEFT JOIN users u ON u.id = a.author_id
WHERE a.slug = $2 AND a.status = 'published' AND ($3 = '' OR a.locale = $3)
ORDER BY c.sort_order, a.sort_order, a.id
LIMIT 1;
-- name: get-published-article-translations
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
SELECT a.id, a.collection_id, a.locale, a.slug, a.title, a.status
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale AND c.id IN (SELECT id FROM published_collections)
WHERE a.translation_group_id = (SELECT translation_group_id FROM help_articles WHERE id = $2)
AND a.status = 'published'
ORDER BY a.locale;
-- name: get-published-collection-locales
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
SELECT DISTINCT c.locale
FROM article_collections c
WHERE c.slug = $2 AND c.id IN (SELECT id FROM published_collections);
-- name: get-published-articles
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
SELECT a.id, a.created_at, a.updated_at, a.collection_id, a.slug, a.locale, a.title, a.excerpt, '' AS content, a.sort_order, a.status, a.view_count, a.ai_enabled
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale AND c.id IN (SELECT id FROM published_collections)
WHERE a.status = 'published' AND ($2 = '' OR a.locale = $2)
ORDER BY a.view_count DESC, a.created_at DESC
LIMIT $3;
-- name: get-published-articles-by-collection
SELECT a.id, a.created_at, a.updated_at, a.collection_id, a.slug, a.locale, a.title, a.excerpt, '' AS content, a.sort_order, a.status, a.view_count, a.ai_enabled
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale
WHERE a.collection_id = $1 AND a.id != $2 AND a.status = 'published' AND ($3 = '' OR a.locale = $3)
ORDER BY a.sort_order ASC, a.created_at DESC
LIMIT $4;
-- name: search-published-articles
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
SELECT a.id, a.created_at, a.updated_at, a.collection_id, a.slug, a.locale, a.title,
a.excerpt AS content,
a.sort_order, a.status, a.view_count, a.ai_enabled
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale AND c.id IN (SELECT id FROM published_collections)
WHERE a.status = 'published' AND a.locale = $4
AND (a.search_tsv @@ to_tsquery(help_article_search_config($4), NULLIF($5, ''))
OR a.title ILIKE $2 ESCAPE '\' OR a.content ILIKE $2 ESCAPE '\')
ORDER BY ts_rank(a.search_tsv, to_tsquery(help_article_search_config($4), NULLIF($5, '')), 2) DESC, a.view_count DESC, a.created_at DESC
LIMIT $3;
-- name: increment-article-view-count
UPDATE help_articles
SET view_count = view_count + 1
WHERE id = $1;
-- name: increment-published-article-view-count
WITH RECURSIVE published_collections AS (
SELECT c.id FROM article_collections c
JOIN help_centers h ON h.id = c.help_center_id
WHERE h.slug = $1 AND c.parent_id IS NULL AND c.is_published = true
UNION
SELECT c.id FROM article_collections c
JOIN published_collections p ON c.parent_id = p.id
WHERE c.is_published = true
)
UPDATE help_articles
SET view_count = view_count + 1
WHERE id = (
SELECT a.id
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale AND c.id IN (SELECT id FROM published_collections)
WHERE a.slug = $2 AND a.status = 'published' AND ($3 = '' OR a.locale = $3)
ORDER BY c.sort_order, a.sort_order, a.id
LIMIT 1
);
-- name: insert-article-feedback
INSERT INTO help_article_feedback (article_id, is_helpful)
SELECT $1, $2
WHERE EXISTS (SELECT 1 FROM help_articles WHERE id = $1 AND status = 'published');
-- name: insert-search-query
INSERT INTO help_search_queries (help_center_id, query, results_count)
VALUES ($1, $2, $3);
-- name: get-top-search-terms
SELECT LOWER(query) AS query, COUNT(*) AS count,
COUNT(*) FILTER (WHERE results_count = 0) AS no_results,
MAX(created_at)::text AS last_search
FROM help_search_queries
WHERE help_center_id = $1 AND created_at >= NOW() - ($3 * INTERVAL '1 day')
GROUP BY LOWER(query)
ORDER BY count DESC, last_search DESC
LIMIT $2;
-- name: get-no-result-search-terms
SELECT LOWER(query) AS query, COUNT(*) AS count,
COUNT(*) AS no_results,
MAX(created_at)::text AS last_search
FROM help_search_queries
WHERE help_center_id = $1 AND results_count = 0 AND created_at >= NOW() - ($3 * INTERVAL '1 day')
GROUP BY LOWER(query)
ORDER BY count DESC, last_search DESC
LIMIT $2;
-- name: delete-stale-search-queries
DELETE FROM help_search_queries
WHERE created_at < NOW() - ($1 * INTERVAL '1 day');
-- name: get-article-references
WITH RECURSIVE article_ancestors AS (
SELECT a.id AS article_id, c.id, c.parent_id, c.help_center_id, c.is_published
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id AND c.locale = a.locale
WHERE a.id = ANY($1) AND a.status = 'published' AND a.ai_enabled = true
UNION
SELECT p.article_id, c.id, c.parent_id, c.help_center_id, c.is_published
FROM article_collections c
JOIN article_ancestors p ON c.id = p.parent_id AND c.help_center_id = p.help_center_id
)
SELECT a.id, a.title, a.slug, a.locale, hc.slug AS help_center_slug, hc.custom_domain
FROM help_articles a
JOIN article_collections c ON c.id = a.collection_id
JOIN help_centers hc ON hc.id = c.help_center_id
WHERE a.id = ANY($1) AND a.status = 'published' AND a.ai_enabled = true AND hc.is_active = true
AND (a.locale = hc.default_locale OR hc.allowed_locales ? a.locale)
AND EXISTS (SELECT 1 FROM article_ancestors p WHERE p.article_id = a.id AND p.parent_id IS NULL)
AND NOT EXISTS (SELECT 1 FROM article_ancestors p WHERE p.article_id = a.id AND p.is_published = false)
ORDER BY array_position($1::integer[], a.id);