godcrm/backend/services/widget-library/browsing.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

373 lines
12 KiB
JavaScript

/**
* Widget Library — Browsing & Filtering
*
* getLibraryWidgets, parseItems, getCategoryCounts
*/
import { dbGet, dbAll, safeJsonParse } from '../../database/connection.js';
/**
* Parse items JSON fields
*/
export function parseItems(items) {
return items.map(item => ({
...item,
config: safeJsonParse(item.config) || {},
tags: safeJsonParse(item.tags) || [],
is_own_space: Boolean(item.is_own_space),
is_favorite: Boolean(item.is_favorite),
is_public: Boolean(item.is_public),
is_template: Boolean(item.is_template)
}));
}
/**
* Get category counts for the widget library sidebar
*/
export async function getCategoryCounts(spaceId, userId, include_public = 0) {
// Count favorites
const favoritesCount = await dbGet(`
SELECT COUNT(DISTINCT uwf.widget_id) as count
FROM user_widget_favorites uwf
JOIN widget_library wl ON wl.widget_id = uwf.widget_id
WHERE uwf.user_id = ?
AND (wl.space_id = ? OR (wl.is_public = true AND ?))
`, [userId || 0, spaceId, include_public]);
// Count recent
const recentCount = await dbGet(`
SELECT COUNT(DISTINCT uwh.widget_id) as count
FROM user_widget_history uwh
JOIN widget_library wl ON wl.widget_id = uwh.widget_id
WHERE uwh.user_id = ?
AND (wl.space_id = ? OR (wl.is_public = true AND ?))
`, [userId || 0, spaceId, include_public]);
// Count this_space
const thisSpaceCount = await dbGet(`
SELECT COUNT(*) as count
FROM widget_library
WHERE space_id = ?
`, [spaceId]);
// Count all_spaces (current + public)
const allSpacesCount = await dbGet(`
SELECT COUNT(*) as count
FROM widget_library
WHERE space_id = ? OR is_public = true
`, [spaceId]);
return {
favorites: Number(favoritesCount?.count) || 0,
recent: Number(recentCount?.count) || 0,
this_space: Number(thisSpaceCount?.count) || 0,
all_spaces: Number(allSpacesCount?.count) || 0
};
}
/**
* Get library widgets with filters
* @param {number} spaceId - Current space
* @param {Object} options - Filter options
* @param {boolean} options.include_public - Include public widgets from other spaces
* @param {string} options.category - Filter by category: 'favorites' | 'recent' | 'this_space' | 'all_spaces'
* @param {string} options.search - Search by title or tags
* @param {number} options.limit - Pagination limit
* @param {number} options.offset - Pagination offset
* @param {number} options.userId - Current user ID for favorites/recent
* @returns {Promise<Object>} { items, total, categories }
*/
export async function getLibraryWidgets(spaceId, options = {}) {
const {
include_public: _include_public = false,
category = null,
search = null,
limit = 50,
offset = 0,
userId = null
} = options;
const include_public = _include_public ? 1 : 0;
let baseQuery = '';
let whereClause = '';
const params = [];
// Handle category filtering
if (category === 'favorites') {
// Only favorites
baseQuery = `
SELECT
wl.id as library_id,
wl.widget_id,
wl.space_id,
wl.is_public,
wl.is_template,
wl.use_count,
wl.last_used_at,
wl.tags,
w.title,
w.preset_name,
w.icon,
w.config,
w.widget_type,
s.name as space_name,
(wl.space_id = ?) as is_own_space,
1 as is_favorite
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE (wl.space_id = ? OR (wl.is_public = true AND ?))
`;
params.push(spaceId, userId || 0, spaceId, include_public);
} else if (category === 'recent') {
// Only recently used widgets
baseQuery = `
SELECT DISTINCT
wl.id as library_id,
wl.widget_id,
wl.space_id,
wl.is_public,
wl.is_template,
wl.use_count,
wl.last_used_at,
wl.tags,
w.title,
w.preset_name,
w.icon,
w.config,
w.widget_type,
s.name as space_name,
(wl.space_id = ?) as is_own_space,
(uwf.id IS NOT NULL) as is_favorite,
MAX(uwh.accessed_at) as recent_access
FROM user_widget_history uwh
JOIN widget_library wl ON wl.widget_id = uwh.widget_id
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
LEFT JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE uwh.user_id = ?
AND (wl.space_id = ? OR (wl.is_public = true AND ?))
`;
params.push(spaceId, userId || 0, userId, spaceId, include_public);
} else if (category === 'this_space') {
// Only current space
baseQuery = `
SELECT
wl.id as library_id,
wl.widget_id,
wl.space_id,
wl.is_public,
wl.is_template,
wl.use_count,
wl.last_used_at,
wl.tags,
w.title,
w.preset_name,
w.icon,
w.config,
w.widget_type,
s.name as space_name,
1 as is_own_space,
(uwf.id IS NOT NULL) as is_favorite
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
LEFT JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE wl.space_id = ?
`;
params.push(userId || 0, spaceId);
} else {
// all_spaces or default: current space + optionally public
if (include_public) {
baseQuery = `
SELECT
wl.id as library_id,
wl.widget_id,
wl.space_id,
wl.is_public,
wl.is_template,
wl.use_count,
wl.last_used_at,
wl.tags,
w.title,
w.preset_name,
w.icon,
w.config,
w.widget_type,
s.name as space_name,
(wl.space_id = ?) as is_own_space,
(uwf.id IS NOT NULL) as is_favorite
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
LEFT JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE (wl.space_id = ? OR wl.is_public = true)
`;
params.push(spaceId, userId || 0, spaceId);
} else {
baseQuery = `
SELECT
wl.id as library_id,
wl.widget_id,
wl.space_id,
wl.is_public,
wl.is_template,
wl.use_count,
wl.last_used_at,
wl.tags,
w.title,
w.preset_name,
w.icon,
w.config,
w.widget_type,
s.name as space_name,
1 as is_own_space,
(uwf.id IS NOT NULL) as is_favorite
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
LEFT JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE wl.space_id = ?
`;
params.push(userId || 0, spaceId);
}
}
// Add search filter
if (search) {
const searchPattern = `%${search}%`;
baseQuery += ` AND (w.title LIKE ? OR wl.tags::text LIKE ?)`;
params.push(searchPattern, searchPattern);
}
// Build count query - copy params before adding pagination
const countParams = [...params];
// Extract WHERE clause for count query
const whereMatch = baseQuery.match(/WHERE[\s\S]*$/);
whereClause = whereMatch ? whereMatch[0] : '';
// Build count query based on category
let countQuery;
if (category === 'recent') {
countQuery = `
SELECT COUNT(DISTINCT wl.id) as total
FROM user_widget_history uwh
JOIN widget_library wl ON wl.widget_id = uwh.widget_id
JOIN widgets w ON w.id = wl.widget_id
JOIN spaces s ON s.id = wl.space_id
LEFT JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE uwh.user_id = ?
AND (wl.space_id = ? OR (wl.is_public = true AND ?))
`;
// For recent, we need to rebuild countParams
const recentCountParams = [userId || 0, userId, spaceId, include_public];
if (search) {
countQuery += ` AND (w.title LIKE ? OR wl.tags::text LIKE ?)`;
recentCountParams.push(`%${search}%`, `%${search}%`);
}
const countResult = await dbGet(countQuery, recentCountParams);
const total = Number(countResult?.total) || 0;
// Add ordering and pagination
baseQuery += ` GROUP BY wl.id, wl.widget_id, wl.space_id, wl.is_public, wl.is_template, wl.use_count, wl.last_used_at, wl.tags, w.title, w.preset_name, w.icon, w.config, w.widget_type, s.name, uwf.id ORDER BY recent_access DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const items = await dbAll(baseQuery, params);
const categories = await getCategoryCounts(spaceId, userId, include_public);
return {
items: parseItems(items),
total,
categories
};
}
// For non-recent queries
if (category === 'favorites') {
countQuery = `
SELECT COUNT(DISTINCT wl.id) as total
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
JOIN user_widget_favorites uwf ON uwf.widget_id = wl.widget_id AND uwf.user_id = ?
WHERE (wl.space_id = ? OR (wl.is_public = true AND ?))
`;
const favCountParams = [userId || 0, spaceId, include_public];
if (search) {
countQuery += ` AND (w.title LIKE ? OR wl.tags::text LIKE ?)`;
favCountParams.push(`%${search}%`, `%${search}%`);
}
const countResult = await dbGet(countQuery, favCountParams);
const total = Number(countResult?.total) || 0;
baseQuery += ` ORDER BY wl.use_count DESC, wl.last_used_at DESC, wl.id DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const items = await dbAll(baseQuery, params);
const categories = await getCategoryCounts(spaceId, userId, include_public);
return {
items: parseItems(items),
total,
categories
};
}
// Default and this_space
if (category === 'this_space' || !include_public) {
countQuery = `
SELECT COUNT(*) as total
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
WHERE wl.space_id = ?
`;
const defaultCountParams = [spaceId];
if (search) {
countQuery += ` AND (w.title LIKE ? OR wl.tags::text LIKE ?)`;
defaultCountParams.push(`%${search}%`, `%${search}%`);
}
const countResult = await dbGet(countQuery, defaultCountParams);
const total = Number(countResult?.total) || 0;
baseQuery += ` ORDER BY wl.use_count DESC, wl.last_used_at DESC, wl.id DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const items = await dbAll(baseQuery, params);
const categories = await getCategoryCounts(spaceId, userId, include_public);
return {
items: parseItems(items),
total,
categories
};
}
// all_spaces with public
countQuery = `
SELECT COUNT(*) as total
FROM widget_library wl
JOIN widgets w ON w.id = wl.widget_id
WHERE (wl.space_id = ? OR wl.is_public = true)
`;
const allCountParams = [spaceId];
if (search) {
countQuery += ` AND (w.title LIKE ? OR wl.tags::text LIKE ?)`;
allCountParams.push(`%${search}%`, `%${search}%`);
}
const countResult = await dbGet(countQuery, allCountParams);
const total = Number(countResult?.total) || 0;
baseQuery += ` ORDER BY wl.use_count DESC, wl.last_used_at DESC, wl.id DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const items = await dbAll(baseQuery, params);
const categories = await getCategoryCounts(spaceId, userId, include_public);
return {
items: parseItems(items),
total,
categories
};
}