Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: feature flag script #823

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -211,4 +211,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';
import sanJoseRedlined from '../data/SanJoseRedlined.json';
Expand All @@ -39,9 +40,10 @@ import district10 from '../data/SanJoseDistrict10.json';
@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 @@ -570,6 +572,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 @@ -782,4 +809,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 @@ -792,6 +796,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
Loading