Skip to content

Commit

Permalink
feat: feature flag migration script (#4499)
Browse files Browse the repository at this point in the history
* feat: adjust update functionality

* feat: create feature flag script

4460

* feat: remove deprecated flags

* feat: remove soon to be deprecated flag

* feat: add description

* feat: include enablePartnerSettings

4460
  • Loading branch information
mcgarrye authored Dec 10, 2024
1 parent 0673c76 commit 959cbb3
Show file tree
Hide file tree
Showing 12 changed files with 375 additions and 177 deletions.
11 changes: 11 additions & 0 deletions api/src/controllers/script-runner.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,15 @@ export class ScirptRunnerController {
): Promise<SuccessDTO> {
return await this.scriptRunnerService.hideProgramsFromListings(req);
}

@Put('addFeatureFlags')
@ApiOperation({
summary:
'A script that adds existing feature flags into the feature flag table',
operationId: 'addFeatureFlags',
})
@ApiOkResponse({ type: SuccessDTO })
async addFeatureFlags(@Request() req: ExpressRequest): Promise<SuccessDTO> {
return await this.scriptRunnerService.addFeatureFlags(req);
}
}
9 changes: 7 additions & 2 deletions api/src/dtos/feature-flags/feature-flag-create.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { OmitType } from '@nestjs/swagger';
import { FeatureFlagUpdate } from './feature-flag-update.dto';
import { FeatureFlag } from './feature-flag.dto';

export class FeatureFlagCreate extends OmitType(FeatureFlagUpdate, ['id']) {}
export class FeatureFlagCreate extends OmitType(FeatureFlag, [
'id',
'createdAt',
'updatedAt',
'jurisdictions',
]) {}
1 change: 1 addition & 0 deletions api/src/dtos/feature-flags/feature-flag-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { FeatureFlag } from './feature-flag.dto';
export class FeatureFlagUpdate extends OmitType(FeatureFlag, [
'createdAt',
'updatedAt',
'name',
'jurisdictions',
]) {}
15 changes: 11 additions & 4 deletions api/src/modules/script-runner.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Module } from '@nestjs/common';
import { ScirptRunnerController } from '../controllers/script-runner.controller';
import { ScriptRunnerService } from '../services/script-runner.service';
import { PrismaModule } from './prisma.module';
import { PermissionModule } from './permission.module';
import { EmailModule } from './email.module';
import { AmiChartModule } from './ami-chart.module';
import { FeatureFlagModule } from './feature-flag.module';
import { EmailModule } from './email.module';
import { PermissionModule } from './permission.module';
import { PrismaModule } from './prisma.module';

@Module({
imports: [PrismaModule, PermissionModule, EmailModule, AmiChartModule],
imports: [
AmiChartModule,
EmailModule,
FeatureFlagModule,
PermissionModule,
PrismaModule,
],
controllers: [ScirptRunnerController],
providers: [ScriptRunnerService],
exports: [ScriptRunnerService],
Expand Down
134 changes: 130 additions & 4 deletions api/src/services/script-runner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import {
ReviewOrderTypeEnum,
} from '@prisma/client';
import { Request as ExpressRequest } from 'express';
import { AmiChartService } from './ami-chart.service';
import { EmailService } from './email.service';
import { FeatureFlagService } from './feature-flag.service';
import { PrismaService } from './prisma.service';
import { SuccessDTO } from '../dtos/shared/success.dto';
import { User } from '../dtos/users/user.dto';
import { mapTo } from '../utilities/mapTo';
import { DataTransferDTO } from '../dtos/script-runner/data-transfer.dto';
import { BulkApplicationResendDTO } from '../dtos/script-runner/bulk-application-resend.dto';
import { EmailService } from './email.service';
import { Application } from '../dtos/applications/application.dto';
import { AmiChartImportDTO } from '../dtos/script-runner/ami-chart-import.dto';
import { AmiChartCreate } from '../dtos/ami-charts/ami-chart-create.dto';
import { AmiChartService } from './ami-chart.service';
import { AmiChartUpdate } from '../dtos/ami-charts/ami-chart-update.dto';
import { AmiChartUpdateImportDTO } from '../dtos/script-runner/ami-chart-update-import.dto';

Expand All @@ -28,9 +29,10 @@ import { AmiChartUpdateImportDTO } from '../dtos/script-runner/ami-chart-update-
@Injectable()
export class ScriptRunnerService {
constructor(
private prisma: PrismaService,
private emailService: EmailService,
private amiChartService: AmiChartService,
private emailService: EmailService,
private featureFlagService: FeatureFlagService,
private prisma: PrismaService,
) {}

/**
Expand Down Expand Up @@ -432,6 +434,31 @@ export class ScriptRunnerService {
return { success: true };
}

/**
Adds all existing feature flags across Bloom to the database
*/
async addFeatureFlags(req: ExpressRequest): Promise<SuccessDTO> {
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart('add feature flags', requestingUser);

const results = await Promise.all(
this.featureFlags.map(async (flag) => {
try {
await this.featureFlagService.create(flag);
} catch (e) {
console.log(
`feature flag ${flag.name} failed to be created. Error: ${e}`,
);
}
}),
);

console.log(`Number of feature flags created: ${results.length}`);

await this.markScriptAsComplete('add feature flags', requestingUser);
return { success: true };
}

/**
this is simply an example
*/
Expand Down Expand Up @@ -644,4 +671,103 @@ export class ScriptRunnerService {
await updateForLanguage(LanguagesEnum.vi, viKeys);
await updateForLanguage(LanguagesEnum.zh, zhKeys);
}

featureFlags = [
{
name: 'enableSingleUseCode',
description:
'When true, the backend allows for logging into this jurisdiction using the single use code flow',
active: false,
},
{
name: 'enableAccessibiliyFeatures',
description:
"When true, the 'accessibility features' section is displayed in listing creation/edit and the public listing view",
active: false,
},
{
name: 'enableGeocodingPreferences',
description:
'When true, preferences can be created with geocoding functionality and when an application is created/updated on a listing that is geocoding then the application gets geocoded',
active: false,
},
{
name: 'enableGeocodingRadiusMethod',
description:
'When true, preferences can be created with geocoding functionality that verifies via a mile radius',
active: false,
},
{
name: 'enableListingOpportunity',
description:
"When true, any newly published listing will send a gov delivery email to everyone that has signed up for the 'listing alerts'",
active: false,
},
{
name: 'enablePartnerDemographics',
description:
'When true, demographics data is included in application or lottery exports for partners',
active: false,
},
{
name: 'enablePartnerSettings',
description:
"When true, the 'settings' tab in the partner site is visible",
active: false,
},
{
name: 'enableUtilitiesIncluded',
description:
"When true, the 'utilities included' section is displayed in listing creation/edit and the public listing view",
active: false,
},
{
name: 'exportApplicationAsSpreadsheet',
description:
'When true, the application export is done as an Excel spreadsheet',
active: false,
},
{
name: 'limitClosedListingActions',
description:
'When true, availability of edit, republish, and reopen functionality is limited for closed listings',
active: false,
},
{
name: 'showLottery',
description:
'When true, show lottery tab on lottery listings on the partners site',
active: false,
},
{
name: 'showMandatedAccounts',
description:
'When true, require users to be logged in to submit an application on the public site',
active: false,
},
{
name: 'showProfessionalPartners',
description:
'When true, show a navigation bar link to professional partners',
active: false,
},
{
name: 'showPublicLottery',
description:
'When true, show lottery section on the user applications page',
active: false,
},
{
name: 'showPwdless',
description:
"When true, show the 'get code to sign in' button on public sign in page for the pwdless flow",
active: false,
},
{
name: 'showSmsMfa',
description:
"When true, show the 'sms' button option when a user goes through multi factor authentication",
active: false,
},
];
}
3 changes: 1 addition & 2 deletions api/test/integration/feature-flag.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ describe('Feature Flag Controller Tests', () => {

const body = {
id: featureFlag.id,
name: `updated ${randomName()}`,
description: 'updated description',
active: !featureFlag.active,
};
Expand All @@ -120,6 +119,7 @@ describe('Feature Flag Controller Tests', () => {

expect(res.body).toEqual({
...body,
name: featureFlag.name,
jurisdictions: [],
createdAt: expect.anything(),
updatedAt: expect.anything(),
Expand All @@ -129,7 +129,6 @@ describe('Feature Flag Controller Tests', () => {
it('should error when trying to update a feature flag that does not exist', async () => {
const body = {
id: randomUUID(),
name: 'updated name',
description: 'updated description',
active: true,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1452,7 +1452,6 @@ describe('Testing Permissioning of endpoints as Admin User', () => {

const body = {
id: featureFlag.id,
name: 'updated name',
description: 'updated description',
active: !featureFlag.active,
};
Expand Down
13 changes: 5 additions & 8 deletions api/test/unit/services/feature-flag.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,19 @@ describe('Testing feature flag service', () => {
prisma.featureFlags.findFirst = jest.fn().mockResolvedValue(mockedValue);
prisma.featureFlags.update = jest.fn().mockResolvedValue({
...mockedValue,
name: 'updated feature flag 1',
description: 'updated feature flag 1',
});

const params: FeatureFlagUpdate = {
name: 'updated feature flag 1',
id: mockedValue.id,
description: mockedValue.description,
description: 'updated feature flag 1',
active: mockedValue.active,
};

expect(await service.update(params)).toEqual({
id: mockedValue.id,
name: 'updated feature flag 1',
description: mockedValue.description,
name: mockedValue.name,
description: 'updated feature flag 1',
active: mockedValue.active,
createdAt: date,
updatedAt: date,
Expand All @@ -177,8 +176,7 @@ describe('Testing feature flag service', () => {

expect(prisma.featureFlags.update).toHaveBeenCalledWith({
data: {
name: 'updated feature flag 1',
description: mockedValue.description,
description: 'updated feature flag 1',
active: mockedValue.active,
},
include: {
Expand All @@ -201,7 +199,6 @@ describe('Testing feature flag service', () => {

const params: FeatureFlagUpdate = {
id: 'example id',
name: 'example feature flag',
description: 'example description',
active: true,
};
Expand Down
50 changes: 47 additions & 3 deletions api/test/unit/services/script-runner.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
import { randomUUID } from 'crypto';
import { Request as ExpressRequest } from 'express';
import { mockDeep } from 'jest-mock-extended';
import { ScriptRunnerService } from '../../../src/services/script-runner.service';
import { PrismaService } from '../../../src/services/prisma.service';
import { User } from '../../../src/dtos/users/user.dto';
import { EmailService } from '../../../src/services/email.service';
import { AmiChartService } from '../../../src/services/ami-chart.service';
import { EmailService } from '../../../src/services/email.service';
import { FeatureFlagService } from '../../../src/services/feature-flag.service';
import { JurisdictionService } from '../../../src/services/jurisdiction.service';
import { PrismaService } from '../../../src/services/prisma.service';
import { ScriptRunnerService } from '../../../src/services/script-runner.service';

const externalPrismaClient = mockDeep<PrismaClient>();

Expand All @@ -33,6 +35,8 @@ describe('Testing script runner service', () => {
},
},
AmiChartService,
FeatureFlagService,
JurisdictionService,
],
}).compile();

Expand Down Expand Up @@ -727,6 +731,46 @@ describe('Testing script runner service', () => {
});
});

it('should create 16 feature flags', async () => {
const id = randomUUID();
const scriptName = 'add feature flags';

prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.update = jest.fn().mockResolvedValue(null);
prisma.featureFlags.create = jest.fn().mockResolvedValue({ id: 'new id' });

const res = await service.addFeatureFlags({
user: {
id,
} as unknown as User,
} as unknown as ExpressRequest);

expect(res.success).toBe(true);

expect(prisma.scriptRuns.findUnique).toHaveBeenCalledWith({
where: {
scriptName,
},
});
expect(prisma.scriptRuns.create).toHaveBeenCalledWith({
data: {
scriptName,
triggeringUser: id,
},
});
expect(prisma.scriptRuns.update).toHaveBeenCalledWith({
data: {
didScriptRun: true,
triggeringUser: id,
},
where: {
scriptName,
},
});
expect(prisma.featureFlags.create).toHaveBeenCalledTimes(16);
});

// | ---------- HELPER TESTS BELOW ---------- | //
it('should mark script run as started if no script run present in db', async () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
Expand Down
Loading

0 comments on commit 959cbb3

Please sign in to comment.