forked from syntaxbullet/aurorabot
feat(service): add feature flags service layer
Implement service for managing feature flags and access control with methods for checking access, creating/enabling flags, and managing whitelisted users/guilds/roles.
This commit is contained in:
138
shared/modules/feature-flags/feature-flags.service.ts
Normal file
138
shared/modules/feature-flags/feature-flags.service.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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<boolean> => {
|
||||||
|
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
||||||
|
where: eq(featureFlags.name, flagName),
|
||||||
|
});
|
||||||
|
return flag?.enabled ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user