145 lines
4.8 KiB
JavaScript
145 lines
4.8 KiB
JavaScript
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
|
|
};
|