Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
163 lines
4.3 KiB
JavaScript
163 lines
4.3 KiB
JavaScript
/**
|
|
* API v3: Widget Library Routes — ADR-073
|
|
* Endpoints for widget picker system
|
|
*/
|
|
|
|
import express from 'express';
|
|
import {
|
|
getLibraryWidgets,
|
|
getFavorites,
|
|
getRecent,
|
|
toggleFavorite,
|
|
addFromLibrary
|
|
} from '../../services/WidgetLibraryService.js';
|
|
import { apiLogger } from '../../utils/logger.js';
|
|
import { success, created, error, badRequest, notFound } from '../../utils/response.js';
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/v3/widget-library
|
|
* Get library widgets with filters
|
|
* Query params: space_id (required), include_public, category, search, limit, offset
|
|
*/
|
|
router.get('/widget-library', async (req, res) => {
|
|
try {
|
|
const { space_id, include_public, category, search, limit, offset } = req.query;
|
|
|
|
if (!space_id) {
|
|
return badRequest(res, 'space_id query parameter is required');
|
|
}
|
|
|
|
const userId = req.user?.id;
|
|
|
|
const result = await getLibraryWidgets(parseInt(space_id), {
|
|
include_public: include_public === 'true' || include_public === '1',
|
|
category: category || null,
|
|
search: search || null,
|
|
limit: limit ? parseInt(limit) : 50,
|
|
offset: offset ? parseInt(offset) : 0,
|
|
userId
|
|
});
|
|
|
|
success(res, result);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Error fetching widget library');
|
|
error(res, 'INTERNAL_ERROR', err.message);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/widget-library/favorites
|
|
* Get user's favorite widgets
|
|
*/
|
|
router.get('/widget-library/favorites', async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
|
|
if (!userId) {
|
|
return badRequest(res, 'User authentication required');
|
|
}
|
|
|
|
const favorites = await getFavorites(userId);
|
|
success(res, favorites);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Error fetching favorites');
|
|
error(res, 'INTERNAL_ERROR', err.message);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/widget-library/recent
|
|
* Get recently used widgets
|
|
* Query params: limit (default 10)
|
|
*/
|
|
router.get('/widget-library/recent', async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
const { limit } = req.query;
|
|
|
|
if (!userId) {
|
|
return badRequest(res, 'User authentication required');
|
|
}
|
|
|
|
const recent = await getRecent(userId, limit ? parseInt(limit) : 10);
|
|
success(res, recent);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Error fetching recent widgets');
|
|
error(res, 'INTERNAL_ERROR', err.message);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/widget-library/:widgetId/favorite
|
|
* Toggle favorite status for a widget
|
|
*/
|
|
router.post('/widget-library/:widgetId/favorite', async (req, res) => {
|
|
try {
|
|
const { widgetId } = req.params;
|
|
const userId = req.user?.id;
|
|
|
|
if (!userId) {
|
|
return badRequest(res, 'User authentication required');
|
|
}
|
|
|
|
const result = await toggleFavorite(userId, parseInt(widgetId));
|
|
success(res, result);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Error toggling favorite');
|
|
|
|
if (err.message === 'Widget not found') {
|
|
return notFound(res, 'Widget');
|
|
}
|
|
|
|
error(res, 'INTERNAL_ERROR', err.message);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/dashboards/:dashboardId/widgets/from-library
|
|
* Add widget from library to dashboard
|
|
* Body: { source_widget_id, mode: 'reference'|'copy', position: { x, y, w, h } }
|
|
*/
|
|
router.post('/dashboards/:dashboardId/widgets/from-library', async (req, res) => {
|
|
try {
|
|
const { dashboardId } = req.params;
|
|
const { source_widget_id, mode, position } = req.body;
|
|
const userId = req.user?.id;
|
|
|
|
if (!source_widget_id) {
|
|
return badRequest(res, 'source_widget_id is required');
|
|
}
|
|
|
|
if (!mode || !['reference', 'copy'].includes(mode)) {
|
|
return badRequest(res, 'Invalid mode: must be "reference" or "copy"');
|
|
}
|
|
|
|
const result = await addFromLibrary(
|
|
parseInt(dashboardId),
|
|
parseInt(source_widget_id),
|
|
mode,
|
|
position || { x: 0, y: 0, w: 6, h: 4 },
|
|
userId
|
|
);
|
|
|
|
created(res, result);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Error adding widget from library');
|
|
|
|
if (err.message === 'Source widget not found') {
|
|
return notFound(res, 'Source widget');
|
|
}
|
|
if (err.message === 'Dashboard not found') {
|
|
return notFound(res, 'Dashboard');
|
|
}
|
|
if (err.message.includes('Invalid mode')) {
|
|
return badRequest(res, err.message);
|
|
}
|
|
|
|
error(res, 'INTERNAL_ERROR', err.message);
|
|
}
|
|
});
|
|
|
|
export default router;
|