UNPKG

@ministryofjustice/hmpps-digital-prison-reporting-frontend

Version:

The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.

320 lines (317 loc) 13 kB
import localsHelper from '../utils/localsHelper.js'; import { isBooleanFlagEnabledOrMissing } from '../services/featureFlagService.js'; import { getFeatureFlagEvaluationSubject, FEATURE_FLAGS } from '../utils/featureFlagsHelper.js'; import setUpNunjucksFilters from '../setUpNunjucksFilters.js'; import { errorRequestHandler } from '../routes/index.js'; import { getAllMyReports, getAllMyBookmarks } from '../utils/reportStoreHelper.js'; import logger from '../utils/logger.js'; import { getDefinitionsPath } from '../utils/definitionUtils.js'; import { cleanupReports } from '../utils/cleanupMyReports.js'; /** * Middleware helper to populate all locals configuration * to enable DPR lib to operate correctly within a service. * * @param {Services} services * @param {string} layoutPath * @param {Environment} env * @param {DprConfig} [config] * @return {*} {RequestHandler} */ const setupResources = (services, layoutPath, env, config) => { return async (req, res, next) => { populateValidationErrors(req, res); populateSubsMessages(req, res); try { // 1. Initialise our feature flags to app.locals await setFeatureFlags(res, services.featureFlagService); // 2. Initialise definitions, collections and definitions path await populateDefinitions(services, req, res, config); // 3. Initialise enabled service flags to app.locals await initialiseServices(services, res); // 4. Populate user reports state with up to date list await populateRequestedReports(services, res, req); setUpDprPaths(res); setupRequestAwareNunjucks(env, res); setUpNunjucksFilters(env); return next(); } catch (error) { return errorRequestHandler(layoutPath)(error, req, res, next); } }; }; const setupRequestAwareNunjucks = (env, res) => { env.addGlobal('getLocals', () => ({ locals: { ...res.locals, ...res.app.locals } })); }; /** * Sets the feature flags to locals * * @param {Response} res * @param {FeatureFlagService} featureFlagService */ const setFeatureFlags = async (res, featureFlagService) => { if (res.app.locals['featureFlags'] === undefined) { res.app.locals['featureFlags'] = { flags: {}, }; } const subject = getFeatureFlagEvaluationSubject(res); const currentFlags = res.app.locals['featureFlags'].flags; const newFlags = await featureFlagService.evaluateBooleanFlags(FEATURE_FLAGS, subject); const hasChanged = JSON.stringify(currentFlags) !== JSON.stringify(newFlags); if (hasChanged) { res.app.locals['featureFlags'].flags = newFlags; logger.info(`FEATURE FLAGS UPDATED: ${JSON.stringify(newFlags)}`); } }; /** * Populates the validation errors * * @param {Request} req * @param {Response} res */ const populateValidationErrors = (req, res) => { const errors = req.flash(`DPR_ERRORS`); if (errors && errors[0]) { res.locals['validationErrors'] = JSON.parse(errors[0]); } }; const populateSubsMessages = (req, res) => { const subscribedMessage = req.flash(`DPR_SUBSCRIBED`); if (subscribedMessage && subscribedMessage[0]) { res.locals['subscribedMessage'] = JSON.parse(subscribedMessage[0]); } const unsubscribedMessage = req.flash(`DPR_UNSUBSCRIBED`); if (unsubscribedMessage && unsubscribedMessage[0]) { res.locals['unsubscribedMessage'] = JSON.parse(unsubscribedMessage[0]); } }; /** * Derives the definition path and sets the locals * * @param {Request} req * @param {Response} res * @param {DprConfig} [config] */ const deriveDefinitionsPath = (req, res, config) => { // Check definitions path from config const dpdPathFromConfig = config?.dataProductDefinitionsPath; res.locals['dpdPathFromConfig'] = !!dpdPathFromConfig; // Check definitions path from request const dpdPathFromQuery = getDefinitionsPath(req.query) || null; const dpdPathFromBody = req.body?.dataProductDefinitionsPath; const definitionsPathFromQuery = dpdPathFromQuery || dpdPathFromBody; res.locals['dpdPathFromQuery'] = !!definitionsPathFromQuery; // Set the definitions path to locals // - incoming query overwrites config const activeDefinitionsPath = definitionsPathFromQuery || dpdPathFromConfig; if (activeDefinitionsPath) { res.locals['hasDefinitionPath'] = !!activeDefinitionsPath; res.locals['definitionsPath'] = activeDefinitionsPath; res.locals['pathSuffix'] = `?dataProductDefinitionsPath=${res.locals['definitionsPath']}`; logger.info(`DEFINITIONS PATH SET: ${JSON.stringify({ definitionsPath: res.locals['definitionsPath'], dpdPathFromBody: res.locals['dpdPathFromBody'], dpdPathFromQuery: res.locals['dpdPathFromQuery'], })}`); } }; /** * Populates the definitions and sets the local collection * * @param {Services} services * @param {Request} req * @param {Response} res * @param {DprConfig} [config] */ const populateDefinitions = async (services, req, res, config) => { // 1. set the definitions path deriveDefinitionsPath(req, res, config); // 2. set the definitions await setDefinitions(services, req, res, config); // 3. set the collections await setProductCollection(services, req, res); }; /** * Set the current collection in the UI * * - only updates and makes API call if collection ID has changed * * @param {Services} services * @param {Request} req * @param {Response} res */ const setProductCollection = async (services, req, res) => { const { token, dprUser } = localsHelper.getValues(res); const selectedProductCollectionId = await services.productCollectionStoreService.getSelectedProductCollectionId(dprUser.id); const currentId = req.session['currentCollectionId']; const updateCollection = currentId !== selectedProductCollectionId; req.session['currentCollectionId'] = selectedProductCollectionId; const allDefs = req.session['allDefinitions']; res.locals['definitions'] = allDefs ?? []; if (updateCollection) { if (selectedProductCollectionId && allDefs) { const collection = await services.productCollectionService.getProductCollection(token, selectedProductCollectionId); if (collection) { req.session['currentCollection'] = collection; const productIds = collection.products.map(p => p.productId); res.locals['definitions'] = allDefs.filter(def => productIds.includes(def.id)); logger.info(`COLLECTION SET: ${res.locals['definitions'].length}`); return; } } res.locals['definitions'] = allDefs ?? []; } }; /** * Set the full catalogue of definitions to app.locals * * @param {Services} services * @param {Request} req * @param {Response} res * @param {DprConfig} [config] */ const setDefinitions = async (services, req, res, config) => { const { token } = localsHelper.getValues(res); if (shouldRunDefinitionsCheck(req.session, config)) { const defs = await services.reportingService.getDefinitions(token, res.locals['definitionsPath']); if (!defs) return; req.session['allDefinitions'] = defs; logger.info(`Definitions set: ${req.session['allDefinitions'].length}`); recordDefinitionsCheck(req.session); } }; /** * Checks if the get definition endpoint should be run * * @export * @param {{ lastDefinitionsCheck?: number }} session * @param {DprConfig} [config] * @return {*} {boolean} */ function shouldRunDefinitionsCheck(session, config) { const interval = config?.checkDefinitionsInterval; // No config -> always run if (interval === undefined) { return true; } // Explicit override -> always run if (interval === 0) { return true; } const lastRun = session.lastDefinitionsCheck; // Never run before -> run now if (!lastRun) { return true; } // Check interval return Date.now() - lastRun > interval; } /** * Records the last run into the session * * @export * @param {{ lastDefinitionsCheck?: number }} session */ function recordDefinitionsCheck(session) { // eslint-disable-next-line no-param-reassign session.lastDefinitionsCheck = Date.now(); } /** * Sets active service config to app.locals * * @param {Services} services * @param {Response} res */ const initialiseServices = async (services, res) => { const { dprUser } = localsHelper.getValues(res); if (dprUser.id) { // Downloading if (!res.app.locals['downloadingEnabled']) { res.app.locals['downloadingEnabled'] = services.downloadPermissionService.enabled; logger.info(`Init service: downloadPermissionService: ${res.app.locals['downloadingEnabled']}`); } // Bookmarking if (!res.app.locals['bookmarkingEnabled']) { res.app.locals['bookmarkingEnabled'] = services.bookmarkService.enabled; logger.info(`Init service: bookmarkService: ${res.app.locals['bookmarkingEnabled']}`); } // Subscriptions if (!res.app.locals['subscriptionsEnabled']) { res.app.locals['subscriptionsEnabled'] = services.subscriptionStoreService.enabled; logger.info(`Init service: subscriptionStoreService: ${res.app.locals['subscriptionsEnabled']}`); } // Collections if (!res.app.locals['collectionsEnabled']) { res.app.locals['collectionsEnabled'] = services.productCollectionService.enabled; logger.info(`Init service: productCollectionService: ${res.app.locals['collectionsEnabled']}`); } // Missing reports if (!res.app.locals['requestMissingEnabled']) { res.app.locals['requestMissingEnabled'] = services.missingReportService.enabled; logger.info(`Init service: missingReportService: ${res.app.locals['requestMissingEnabled']}`); } // Migration service if (!res.app.locals['reportIdMigrationServiceEnabled']) { res.app.locals['reportIdMigrationServiceEnabled'] = services.reportIdMigrationService.enabled; logger.info(`Init service: reportIdMigrationService: ${res.app.locals['reportIdMigrationServiceEnabled']}`); } // Save defaults const enabled = isBooleanFlagEnabledOrMissing('saveDefaultsEnabled', res.app) ? services.defaultFilterValuesService.enabled : false; const current = res.app.locals['saveDefaultsEnabled']; if (current !== enabled) { res.app.locals['saveDefaultsEnabled'] = enabled; logger.info(`Init service: defaultFilterValuesService: ${res.app.locals['saveDefaultsEnabled']}`); } } }; /** * Populates the requested reports to locals * * @param {Services} services * @param {Response} res */ const populateRequestedReports = async (services, res, req) => { const { dprUser } = localsHelper.getValues(res); if (dprUser.id) { const myReports = await getAllMyReports(res, services, dprUser.id); const { requestedReports, recentlyViewedReports } = await cleanupReports(services, dprUser.id, myReports.requestedReports, myReports.recentlyViewedReports, res); res.locals['requestedReports'] = requestedReports; res.locals['recentlyViewedReports'] = recentlyViewedReports; res.locals['subscriptions'] = myReports.subscriptions; if (res.app.locals['bookmarkingEnabled']) { res.locals['bookmarks'] = await getAllMyBookmarks(res, services, dprUser.id); } const removedReports = req.flash('DPR_REMOVED_REPORTS'); if (removedReports && removedReports[0]) { const [firstRemovedReport] = removedReports; res.locals['removedReports'] = JSON.parse(firstRemovedReport); } } }; /** * Initialises the DPR paths to locals * * @param {Response} res */ const setUpDprPaths = (res) => { res.app.locals.dprPaths ??= { bookmarkActionEndpoint: '/dpr/my-reports/bookmarks', downloadActionEndpoint: '/dpr/download-report/', productCollectionEndpoint: '/dpr/product-collection/selected', bookmarkListPath: '/dpr/my-reports/bookmarks', requestedListPath: '/dpr/my-reports/requested-reports', recentlyViewedListPath: '/dpr/my-reports/recently-viewed', subscribePath: '/dpr/my-reports/subscriptions', reportsCatalogue: '/dpr/report-catalogue', userReportsList: '/dpr/my-reports', requestReportPath: '/dpr/request-report', viewReportPath: '/dpr/view-report', dprHomepage: '/dpr', }; }; export { setupResources as default, recordDefinitionsCheck, setupResources, shouldRunDefinitionsCheck }; //# sourceMappingURL=setUpDprResources.js.map