Files
aurorabot/shared/modules/feature-flags/feature-flags.service.ts
syntaxbullet 5bd390b4ee docs: add JSDoc to service public methods
One-line JSDoc on 82 methods across 11 service files for quick
scanning without reading full implementations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:18 +02:00

149 lines
5.1 KiB
TypeScript

import { eq, or, and, inArray } from "drizzle-orm";
import { featureFlags, featureFlagAccess } from "@db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { UserError } from "@shared/lib/errors";
export interface FeatureFlagContext {
guildId: string;
userId: string;
memberRoles: string[];
}
export const featureFlagsService = {
/** Check whether a feature flag is enabled globally. */
isFlagEnabled: async (flagName: string): Promise<boolean> => {
const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName),
});
return flag?.enabled ?? false;
},
/** Check if a guild/user/role has access to a feature flag; returns false if flag is disabled. */
hasAccess: async (
flagName: string,
context: FeatureFlagContext
): Promise<boolean> => {
const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName),
});
if (!flag || !flag.enabled) return false;
const access = await DrizzleClient.query.featureFlagAccess.findFirst({
where: and(
eq(featureFlagAccess.flagId, flag.id),
or(
eq(featureFlagAccess.guildId, BigInt(context.guildId)),
eq(featureFlagAccess.userId, BigInt(context.userId))
)
),
});
if (access) return true;
if (context.memberRoles.length > 0) {
const roleAccess = await DrizzleClient.query.featureFlagAccess.findFirst({
where: and(
eq(featureFlagAccess.flagId, flag.id),
inArray(featureFlagAccess.roleId, context.memberRoles.map(r => BigInt(r)))
),
});
return !!roleAccess;
}
return false;
},
/** Create a new feature flag, disabled by default. */
createFlag: async (name: string, description?: string) => {
const [flag] = await DrizzleClient.insert(featureFlags).values({
name,
description,
enabled: false,
}).returning();
return flag;
},
/** Enable or disable a feature flag by name; throws if the flag does not exist. */
setFlagEnabled: async (name: string, enabled: boolean) => {
const [flag] = await DrizzleClient.update(featureFlags)
.set({ enabled, updatedAt: new Date() })
.where(eq(featureFlags.name, name))
.returning();
if (!flag) {
throw new UserError(`Feature flag "${name}" not found`);
}
return flag;
},
/** Grant a guild, user, or role access to a feature flag. */
grantAccess: async (
flagName: string,
access: { guildId?: string; userId?: string; roleId?: string }
) => {
const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName),
});
if (!flag) throw new UserError(`Feature flag "${flagName}" not found`);
const [accessRecord] = await DrizzleClient.insert(featureFlagAccess).values({
flagId: flag.id,
guildId: access.guildId ? BigInt(access.guildId) : null,
userId: access.userId ? BigInt(access.userId) : null,
roleId: access.roleId ? BigInt(access.roleId) : null,
}).returning();
return accessRecord;
},
/** Revoke a specific access record by ID; throws if not found. */
revokeAccess: async (accessId: number) => {
const [access] = await DrizzleClient.delete(featureFlagAccess)
.where(eq(featureFlagAccess.id, accessId))
.returning();
if (!access) {
throw new UserError(`Access record "${accessId}" not found`);
}
return access;
},
/** Retrieve a single feature flag by name. */
getFlag: async (name: string) => {
return await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, name),
});
},
/** List all feature flags, ordered by name. */
listFlags: async () => {
return await DrizzleClient.query.featureFlags.findMany({
orderBy: (flags, { asc }) => [asc(flags.name)],
});
},
/** List all access records for a given feature flag. */
listAccess: async (flagName: string) => {
const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName),
});
if (!flag) return [];
return await DrizzleClient.query.featureFlagAccess.findMany({
where: eq(featureFlagAccess.flagId, flag.id),
orderBy: (access, { asc }) => [asc(access.id)],
});
},
/** Delete a feature flag and its associated access records; throws if not found. */
deleteFlag: async (name: string) => {
const [flag] = await DrizzleClient.delete(featureFlags)
.where(eq(featureFlags.name, name))
.returning();
if (!flag) {
throw new UserError(`Feature flag "${name}" not found`);
}
return flag;
},
};