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 = { isFlagEnabled: async (flagName: string): Promise => { const flag = await DrizzleClient.query.featureFlags.findFirst({ where: eq(featureFlags.name, flagName), }); return flag?.enabled ?? false; }, hasAccess: async ( flagName: string, context: FeatureFlagContext ): Promise => { 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; }, createFlag: async (name: string, description?: string) => { const [flag] = await DrizzleClient.insert(featureFlags).values({ name, description, enabled: false, }).returning(); return flag; }, 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; }, 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; }, 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; }, getFlag: async (name: string) => { return await DrizzleClient.query.featureFlags.findFirst({ where: eq(featureFlags.name, name), }); }, listFlags: async () => { return await DrizzleClient.query.featureFlags.findMany({ orderBy: (flags, { asc }) => [asc(flags.name)], }); }, 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)], }); }, 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; }, };