feature/IO-3701-Harness-Replacement - Implement
This commit is contained in:
144
server/feature-flags/feature-flags.js
Normal file
144
server/feature-flags/feature-flags.js
Normal file
@@ -0,0 +1,144 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user