Read feature flags from cache (#11556)
We are now storing a workspace's feature flag map in our redis cache. The cache is invalidated upon feature flag update through the lab resolver.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Column } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
@ObjectType('FeatureFlagDTO')
|
||||
export class FeatureFlagDTO {
|
||||
@Field(() => FeatureFlagKey)
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
key: FeatureFlagKey;
|
||||
|
||||
@Field()
|
||||
@Column({ nullable: false })
|
||||
value: boolean;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,6 +16,7 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
|
||||
services: [],
|
||||
resolvers: [],
|
||||
}),
|
||||
WorkspaceFeatureFlagsMapCacheModule,
|
||||
],
|
||||
exports: [FeatureFlagService],
|
||||
providers: [FeatureFlagService],
|
||||
|
||||
+32
-17
@@ -10,6 +10,7 @@ import {
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate';
|
||||
import { publicFeatureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate',
|
||||
@@ -29,6 +30,11 @@ describe('FeatureFlagService', () => {
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceFeatureFlagsMapCacheService = {
|
||||
getWorkspaceFeatureFlagsMap: jest.fn(),
|
||||
recomputeFeatureFlagsMapCache: jest.fn(),
|
||||
};
|
||||
|
||||
const workspaceId = 'workspace-id';
|
||||
const featureFlag = FeatureFlagKey.IsWorkflowEnabled;
|
||||
|
||||
@@ -47,6 +53,10 @@ describe('FeatureFlagService', () => {
|
||||
provide: getRepositoryToken(FeatureFlag, 'core'),
|
||||
useValue: mockFeatureFlagRepository,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceFeatureFlagsMapCacheService,
|
||||
useValue: mockWorkspaceFeatureFlagsMapCacheService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -60,27 +70,29 @@ describe('FeatureFlagService', () => {
|
||||
describe('isFeatureEnabled', () => {
|
||||
it('should return true when feature flag is enabled', async () => {
|
||||
// Prepare
|
||||
mockFeatureFlagRepository.findOneBy.mockResolvedValue({
|
||||
key: featureFlag,
|
||||
value: true,
|
||||
workspaceId,
|
||||
});
|
||||
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue(
|
||||
{
|
||||
[featureFlag]: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.isFeatureEnabled(featureFlag, workspaceId);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockFeatureFlagRepository.findOneBy).toHaveBeenCalledWith({
|
||||
expect(
|
||||
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap,
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId,
|
||||
key: featureFlag,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when feature flag is not found', async () => {
|
||||
// Prepare
|
||||
mockFeatureFlagRepository.findOneBy.mockResolvedValue(null);
|
||||
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue(
|
||||
{},
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.isFeatureEnabled(featureFlag, workspaceId);
|
||||
@@ -94,7 +106,6 @@ describe('FeatureFlagService', () => {
|
||||
mockFeatureFlagRepository.findOneBy.mockResolvedValue({
|
||||
key: featureFlag,
|
||||
value: false,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -108,21 +119,25 @@ describe('FeatureFlagService', () => {
|
||||
describe('getWorkspaceFeatureFlags', () => {
|
||||
it('should return all feature flags for a workspace', async () => {
|
||||
// Prepare
|
||||
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue(
|
||||
{
|
||||
[FeatureFlagKey.IsWorkflowEnabled]: true,
|
||||
[FeatureFlagKey.IsCopilotEnabled]: false,
|
||||
},
|
||||
);
|
||||
const mockFeatureFlags = [
|
||||
{ key: FeatureFlagKey.IsWorkflowEnabled, value: true, workspaceId },
|
||||
{ key: FeatureFlagKey.IsCopilotEnabled, value: false, workspaceId },
|
||||
{ key: FeatureFlagKey.IsWorkflowEnabled, value: true },
|
||||
{ key: FeatureFlagKey.IsCopilotEnabled, value: false },
|
||||
];
|
||||
|
||||
mockFeatureFlagRepository.find.mockResolvedValue(mockFeatureFlags);
|
||||
|
||||
// Act
|
||||
const result = await service.getWorkspaceFeatureFlags(workspaceId);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockFeatureFlags);
|
||||
expect(mockFeatureFlagRepository.find).toHaveBeenCalledWith({
|
||||
where: { workspaceId },
|
||||
});
|
||||
expect(
|
||||
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap,
|
||||
).toHaveBeenCalledWith({ workspaceId });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+43
-27
@@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag-dto';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import {
|
||||
@@ -13,47 +14,46 @@ import {
|
||||
} from 'src/engine/core-modules/feature-flag/feature-flag.exception';
|
||||
import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate';
|
||||
import { publicFeatureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class FeatureFlagService {
|
||||
constructor(
|
||||
@InjectRepository(FeatureFlag, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlag>,
|
||||
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
|
||||
) {}
|
||||
|
||||
public async isFeatureEnabled(
|
||||
key: FeatureFlagKey,
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const featureFlag = await this.featureFlagRepository.findOneBy({
|
||||
workspaceId,
|
||||
key,
|
||||
value: true,
|
||||
});
|
||||
const featureFlagMap = await this.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
return !!featureFlag?.value;
|
||||
return !!featureFlagMap[key];
|
||||
}
|
||||
|
||||
public async getWorkspaceFeatureFlags(
|
||||
workspaceId: string,
|
||||
): Promise<FeatureFlag[]> {
|
||||
return this.featureFlagRepository.find({ where: { workspaceId } });
|
||||
): Promise<FeatureFlagDTO[]> {
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return Object.entries(workspaceFeatureFlagsMap).map(([key, value]) => ({
|
||||
key: key as FeatureFlagKey,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
public async getWorkspaceFeatureFlagsMap(
|
||||
workspaceId: string,
|
||||
): Promise<FeatureFlagMap> {
|
||||
const workspaceFeatureFlags =
|
||||
await this.getWorkspaceFeatureFlags(workspaceId);
|
||||
|
||||
const workspaceFeatureFlagsMap = workspaceFeatureFlags.reduce(
|
||||
(result, currentFeatureFlag) => {
|
||||
result[currentFeatureFlag.key] = currentFeatureFlag.value;
|
||||
|
||||
return result;
|
||||
},
|
||||
{} as FeatureFlagMap,
|
||||
);
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return workspaceFeatureFlagsMap;
|
||||
}
|
||||
@@ -62,13 +62,21 @@ export class FeatureFlagService {
|
||||
keys: FeatureFlagKey[],
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.featureFlagRepository.upsert(
|
||||
keys.map((key) => ({ workspaceId, key, value: true })),
|
||||
{
|
||||
conflictPaths: ['workspaceId', 'key'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
if (keys.length > 0) {
|
||||
await this.featureFlagRepository.upsert(
|
||||
keys.map((key) => ({ workspaceId, key, value: true })),
|
||||
{
|
||||
conflictPaths: ['workspaceId', 'key'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
|
||||
{
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async upsertWorkspaceFeatureFlag({
|
||||
@@ -120,6 +128,14 @@ export class FeatureFlagService {
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
|
||||
return await this.featureFlagRepository.save(featureFlagToSave);
|
||||
const result = await this.featureFlagRepository.save(featureFlagToSave);
|
||||
|
||||
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
|
||||
{
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user