@peopl-health/nexus 3.5.16 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -16,6 +16,7 @@ const Symptoms_ID = runtimeConfig.get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfY
|
|
|
16
16
|
const Follow_Up_ID = runtimeConfig.get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
|
|
17
17
|
const Webinars_Leads_ID = runtimeConfig.get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
|
|
18
18
|
const Product_ID = runtimeConfig.get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
|
|
19
|
+
const Dashboard_ID = runtimeConfig.get('AIRTABLE_DASHBOARD_ID') || 'appg9YUNlfIC82MzR';
|
|
19
20
|
|
|
20
21
|
let airtable = null;
|
|
21
22
|
if (airtableConfig.apiKey) {
|
|
@@ -32,7 +33,8 @@ const BASE_MAP = {
|
|
|
32
33
|
symptoms: Symptoms_ID,
|
|
33
34
|
followup: Follow_Up_ID,
|
|
34
35
|
webinars: Webinars_Leads_ID,
|
|
35
|
-
product: Product_ID
|
|
36
|
+
product: Product_ID,
|
|
37
|
+
dashboard: Dashboard_ID
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
module.exports = {
|
|
@@ -48,6 +50,7 @@ module.exports = {
|
|
|
48
50
|
Follow_Up_ID,
|
|
49
51
|
Webinars_Leads_ID,
|
|
50
52
|
Product_ID,
|
|
53
|
+
Dashboard_ID,
|
|
51
54
|
getBase: (baseKeyOrId = runtimeConfig.get('AIRTABLE_BASE_ID')) => {
|
|
52
55
|
if (!airtable) {
|
|
53
56
|
throw new Error('Airtable not configured. Please set AIRTABLE_API_KEY environment variable.');
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
const { validateAndAdaptBox } = require('../helpers/dashboardHelper');
|
|
4
|
+
|
|
5
|
+
const { getAllBoxes, getStatsById, updateBox } = require('../services/dashboardService');
|
|
6
|
+
|
|
7
|
+
const getDashboardController = async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const force = req.query.force === 'true';
|
|
10
|
+
const boxes = await getAllBoxes(force);
|
|
11
|
+
res.json({ success: true, boxes });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
logger.error('[DashboardController] Error fetching dashboards:', { error: error.message });
|
|
14
|
+
res.status(500).json({ success: false, error: error.message || 'Failed to fetch dashboards' });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getDashboardStatsControllerById = async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const { id } = req.params;
|
|
21
|
+
const { type } = req.query;
|
|
22
|
+
const cacheId = type ? `${type}_${id}` : id;
|
|
23
|
+
|
|
24
|
+
const stats = await getStatsById(cacheId);
|
|
25
|
+
if (!stats) {
|
|
26
|
+
return res.status(404).json({ success: false, error: 'Indicator stats not found' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.json({ success: true, stats });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.error('[DashboardController] Error fetching dashboard stats by id:', { error: error.message, id: req.params?.id });
|
|
32
|
+
res.status(500).json({ success: false, error: error.message || 'Failed to fetch dashboard stats' });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const updateDashboardControllerById = async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const { error, box } = validateAndAdaptBox(req.body);
|
|
39
|
+
if (error) {
|
|
40
|
+
return res.status(400).json({ success: false, error });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
res.json({ success: true, message: 'Box updated', id: box.id });
|
|
44
|
+
|
|
45
|
+
await updateBox(box.id, box);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error('[DashboardController] Error updating dashboard:', { error: error.message, id: req.params?.id });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
getDashboardController,
|
|
53
|
+
getDashboardStatsControllerById,
|
|
54
|
+
updateDashboardControllerById
|
|
55
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { Dashboard_ID } = require('../config/airtableConfig');
|
|
2
|
+
|
|
3
|
+
const { logger } = require('../utils/logger');
|
|
4
|
+
const { safeParse } = require('../utils/jsonUtils');
|
|
5
|
+
|
|
6
|
+
const { getRecordByFilter } = require('../services/airtableService');
|
|
7
|
+
|
|
8
|
+
const BOX_COLUMNS = ['id', 'type', 'title', 'metadata', 'config', 'total', 'sample', 'indicator'];
|
|
9
|
+
const REQUIRED_BOX_FIELDS = ['id', 'type', 'metadata'];
|
|
10
|
+
|
|
11
|
+
function adaptBox(raw) {
|
|
12
|
+
return {
|
|
13
|
+
...raw,
|
|
14
|
+
id: `${raw['type']}_${raw['id']}`,
|
|
15
|
+
metadata: safeParse(raw['metadata']),
|
|
16
|
+
config: safeParse(raw['config'])
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateAndAdaptBox(body, existingBox) {
|
|
21
|
+
if (!body || typeof body !== 'object') return { error: 'Request body is required' };
|
|
22
|
+
const missing = REQUIRED_BOX_FIELDS.filter(field => !(field in body));
|
|
23
|
+
if (missing.length > 0) return { error: `Missing required fields: ${missing.join(', ')}` };
|
|
24
|
+
|
|
25
|
+
const adapted = adaptBox(body);
|
|
26
|
+
if (!adapted.config && existingBox?.config) {
|
|
27
|
+
adapted.config = existingBox.config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { box: adapted };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function fetchBoxesFromAirtable(boxId) {
|
|
34
|
+
let filter = '{is_active} = true';
|
|
35
|
+
if (boxId) {
|
|
36
|
+
filter = `AND({is_active} = true, {id} = '${boxId}')`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rawBoxes = await getRecordByFilter(Dashboard_ID, 'master', filter, undefined, BOX_COLUMNS) || [];
|
|
40
|
+
return rawBoxes.map(adaptBox);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchDetailsFromAirtable(boxes) {
|
|
44
|
+
if (!boxes || boxes.length === 0) return [];
|
|
45
|
+
|
|
46
|
+
const results = await Promise.all(
|
|
47
|
+
boxes.map(async box => {
|
|
48
|
+
if (!box.config || !box.config.table) {
|
|
49
|
+
logger.warn(`[DashboardHelper] Box ${box.id} has no config or table, skipping`);
|
|
50
|
+
return { id: box.id, records: [] };
|
|
51
|
+
}
|
|
52
|
+
const records = await getRecordByFilter(Dashboard_ID, box.config.table, 'TRUE()', box.config.view, box.config.columns) || [];
|
|
53
|
+
return { id: box.id, records };
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function attachPreview(box, records) {
|
|
60
|
+
if (records.length > 0) {
|
|
61
|
+
box.metadata = { ...box.metadata, preview: records[0] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { fetchBoxesFromAirtable, fetchDetailsFromAirtable, attachPreview, validateAndAdaptBox };
|
package/lib/routes/index.js
CHANGED
|
@@ -80,6 +80,12 @@ const templateRouteDefinitions = {
|
|
|
80
80
|
'DELETE /:id': 'deleteTemplate'
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const dashboardRouteDefinitions = {
|
|
84
|
+
'GET /': 'getDashboardController',
|
|
85
|
+
'GET /stats/:id': 'getDashboardStatsControllerById',
|
|
86
|
+
'POST /:id': 'updateDashboardControllerById'
|
|
87
|
+
};
|
|
88
|
+
|
|
83
89
|
const createRouter = (routeDefinitions, controllers) => {
|
|
84
90
|
const router = express.Router();
|
|
85
91
|
|
|
@@ -109,6 +115,7 @@ const templateController = require('../controllers/templateController');
|
|
|
109
115
|
const templateFlowController = require('../controllers/templateFlowController');
|
|
110
116
|
const flowDataExchangeController = require('../controllers/flowDataExchangeController');
|
|
111
117
|
const uploadController = require('../controllers/uploadController');
|
|
118
|
+
const dashboardController = require('../controllers/dashboardController');
|
|
112
119
|
|
|
113
120
|
const builtInControllers = {
|
|
114
121
|
// Assistant controllers
|
|
@@ -181,7 +188,12 @@ const builtInControllers = {
|
|
|
181
188
|
// Thread review controllers
|
|
182
189
|
updateReviewStatusController: conversationController.updateReviewStatusController,
|
|
183
190
|
updateAllReviewStatusController: conversationController.updateAllReviewStatusController,
|
|
184
|
-
getReviewStatusController: conversationController.getReviewStatusController
|
|
191
|
+
getReviewStatusController: conversationController.getReviewStatusController,
|
|
192
|
+
|
|
193
|
+
// Dashboard controllers
|
|
194
|
+
getDashboardController: dashboardController.getDashboardController,
|
|
195
|
+
getDashboardStatsControllerById: dashboardController.getDashboardStatsControllerById,
|
|
196
|
+
updateDashboardControllerById: dashboardController.updateDashboardControllerById
|
|
185
197
|
};
|
|
186
198
|
|
|
187
199
|
const setupDefaultRoutes = (app) => {
|
|
@@ -192,6 +204,7 @@ const setupDefaultRoutes = (app) => {
|
|
|
192
204
|
app.use('/api/message', createRouter(messageRouteDefinitions, builtInControllers));
|
|
193
205
|
app.use('/api/patient', createRouter(patientRouteDefinitions, builtInControllers));
|
|
194
206
|
app.use('/api/template', createRouter(templateRouteDefinitions, builtInControllers));
|
|
207
|
+
app.use('/api/dashboard', createRouter(dashboardRouteDefinitions, builtInControllers));
|
|
195
208
|
};
|
|
196
209
|
|
|
197
210
|
module.exports = {
|
|
@@ -202,6 +215,7 @@ module.exports = {
|
|
|
202
215
|
messageRoutes: messageRouteDefinitions,
|
|
203
216
|
patientRoutes: patientRouteDefinitions,
|
|
204
217
|
templateRoutes: templateRouteDefinitions,
|
|
218
|
+
dashboardRoutes: dashboardRouteDefinitions,
|
|
205
219
|
|
|
206
220
|
createRouter,
|
|
207
221
|
setupDefaultRoutes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
2
|
+
const MapCache = require('../utils/MapCache');
|
|
3
|
+
|
|
4
|
+
const { fetchBoxesFromAirtable, fetchDetailsFromAirtable, attachPreview } = require('../helpers/dashboardHelper');
|
|
5
|
+
|
|
6
|
+
const boxCache = new MapCache({ maxSize: 100 });
|
|
7
|
+
const detailCache = new MapCache({ maxSize: 100 });
|
|
8
|
+
|
|
9
|
+
async function seedCaches() {
|
|
10
|
+
try {
|
|
11
|
+
logger.info('[DashboardService] Cold-start: seeding caches from Airtable');
|
|
12
|
+
|
|
13
|
+
const boxes = await fetchBoxesFromAirtable();
|
|
14
|
+
boxes.forEach(box => boxCache.set(box.id, box));
|
|
15
|
+
|
|
16
|
+
const details = await fetchDetailsFromAirtable(boxes);
|
|
17
|
+
details.forEach(({ id, records }) => {
|
|
18
|
+
detailCache.set(id, records);
|
|
19
|
+
const box = boxCache.get(id);
|
|
20
|
+
if (box) attachPreview(box, records);
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error('[DashboardService] Error seeding caches:', { error: error.message });
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureSeeded(force) {
|
|
29
|
+
if (force) {
|
|
30
|
+
boxCache.clear();
|
|
31
|
+
detailCache.clear();
|
|
32
|
+
}
|
|
33
|
+
if (boxCache.size === 0) {
|
|
34
|
+
await seedCaches();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getAllBoxes(force) {
|
|
39
|
+
await ensureSeeded(force);
|
|
40
|
+
return Array.from(boxCache.entries()).map(([id, { config: _config, ...data }]) => ({ id, ...data }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getStatsById(cacheId) {
|
|
44
|
+
const detail = detailCache.get(cacheId);
|
|
45
|
+
if (!detail) return null;
|
|
46
|
+
return { id: cacheId, records: detail };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function updateBox(id, data) {
|
|
50
|
+
boxCache.set(id, data);
|
|
51
|
+
logger.info('[DashboardService] Box cache updated', { id });
|
|
52
|
+
|
|
53
|
+
const box = boxCache.get(id);
|
|
54
|
+
if (box?.config?.table) {
|
|
55
|
+
const [detail] = await fetchDetailsFromAirtable([box]);
|
|
56
|
+
if (detail) {
|
|
57
|
+
detailCache.set(detail.id, detail.records);
|
|
58
|
+
attachPreview(box, detail.records);
|
|
59
|
+
logger.info('[DashboardService] Detail cache refreshed after box update', { id });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
getAllBoxes,
|
|
66
|
+
getStatsById,
|
|
67
|
+
updateBox
|
|
68
|
+
};
|
package/lib/utils/jsonUtils.js
CHANGED
|
@@ -9,4 +9,12 @@ function jsonStringifyWithUnicodeEscapes(obj) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function safeParse(value, fallback = {}) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value || JSON.stringify(fallback));
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { jsonStringifyWithUnicodeEscapes, safeParse };
|