const logger = require("../utils/logger"); const { CHECK_BODYSHOP_ACCESS, GET_BODYSHOP_FEATURE_FLAGS } = require("../graphql-client/queries"); const { emitFeatureFlagsChanged } = require("./socket-events"); /** * Indicates whether verbose feature flag route logging should be enabled. */ const isDevelopment = () => process.env.NODE_ENV === "development"; /** * Combines global feature flag definitions with per-bodyshop assignments into the runtime flag map. */ const toFlagMap = ({ feature_flags: definitions = [], bodyshop_feature_flags: assignments = [] }) => { const flags = definitions.reduce((acc, definition) => { acc[definition.name] = { treatment: definition.default_treatment || "off", config: null }; return acc; }, {}); for (const assignment of assignments) { flags[assignment.name] = { treatment: assignment.treatment, config: assignment.config ?? null, activeDate: assignment.activeDate ?? null, deactiveDate: assignment.deactiveDate ?? null }; } return flags; }; /** * Verifies that the authenticated user can read the requested bodyshop through Hasura permissions. */ async function assertBodyshopAccess({ req, bodyshopId }) { const result = await req.userGraphQLClient.request(CHECK_BODYSHOP_ACCESS, { id: bodyshopId }); if (!result.bodyshops_by_pk?.id) { const error = new Error("Feature flag bodyshop access denied"); error.statusCode = 403; throw error; } } /** * Serves runtime feature flags for one bodyshop with Redis read-through caching. */ async function getBodyshopFeatureFlags(req, res) { const bodyshopId = req.params.bodyshopId; const { getBodyshopFeatureFlagsCacheVersion, getBodyshopFeatureFlagsFromRedis, setBodyshopFeatureFlagsInRedis } = req.sessionUtils || {}; try { await assertBodyshopAccess({ req, bodyshopId }); const cacheVersion = getBodyshopFeatureFlagsCacheVersion ? await getBodyshopFeatureFlagsCacheVersion() : null; const cachedFlags = getBodyshopFeatureFlagsFromRedis ? await getBodyshopFeatureFlagsFromRedis(bodyshopId, cacheVersion) : null; if (cachedFlags) { if (isDevelopment()) { logger.log("feature-flags-route-hit", "DEBUG", req.user?.email, null, { bodyshopId, source: "redis", flagCount: Object.keys(cachedFlags.flags || {}).length }); } return res.json({ ...cachedFlags, source: "redis" }); } const result = await req.userGraphQLClient.request(GET_BODYSHOP_FEATURE_FLAGS, { bodyshopid: bodyshopId }); const payload = { bodyshopId, flags: toFlagMap(result), cachedAt: new Date().toISOString() }; if (setBodyshopFeatureFlagsInRedis) { await setBodyshopFeatureFlagsInRedis(bodyshopId, payload, cacheVersion); } if (isDevelopment()) { logger.log("feature-flags-route-hit", "DEBUG", req.user?.email, null, { bodyshopId, source: "database", flagCount: Object.keys(payload.flags || {}).length }); } return res.json({ ...payload, source: "database" }); } catch (error) { const statusCode = error.statusCode || 500; logger.log("get-bodyshop-feature-flags-error", "ERROR", req.user?.email, null, { bodyshopId, statusCode, error: error.message, stack: error.stack }); return res.status(statusCode).json({ error: error.message }); } } /** * Handles Hasura/admin cache invalidation events and notifies connected clients. */ async function invalidateBodyshopFeatureFlags(req, res) { const bodyshopId = req.body?.event?.data?.new?.bodyshopid || req.body?.event?.data?.old?.bodyshopid || req.body?.bodyshopid; const tableName = req.body?.event?.table?.name; const flagName = req.body?.event?.data?.new?.name || req.body?.event?.data?.old?.name || req.body?.name || null; try { if (bodyshopId && req.sessionUtils?.invalidateBodyshopFeatureFlagsInRedis) { await req.sessionUtils.invalidateBodyshopFeatureFlagsInRedis(bodyshopId); emitFeatureFlagsChanged({ req, bodyshopId, source: "hasura", table: tableName, name: flagName }); return res.status(200).json({ ok: true, bodyshopId }); } const invalidated = await req.sessionUtils?.invalidateAllBodyshopFeatureFlagsInRedis?.(); emitFeatureFlagsChanged({ req, source: "hasura", table: tableName, name: flagName }); return res.status(200).json({ ok: true, table: tableName, cacheVersion: invalidated || 0 }); } catch (error) { logger.log("invalidate-bodyshop-feature-flags-error", "ERROR", "feature-flags", null, { bodyshopId, tableName, error: error.message, stack: error.stack }); return res.status(500).json({ error: error.message }); } } module.exports = { getBodyshopFeatureFlags, invalidateBodyshopFeatureFlags };