Skip to content

Commit

Permalink
feat: feature flag consumption (bloom-housing#4489) (#820)
Browse files Browse the repository at this point in the history
* feat: feature flag controller service and tests

4459

* feat: associate jurisdictions tests

* feat: permissions and permission tests

* feat: make naming random

* feat: edge case coverage

* feat: refine uuid array validation

* feat: remove unused import

* feat: controller cleanup
  • Loading branch information
mcgarrye authored Dec 2, 2024
1 parent 061c810 commit 5261ae8
Show file tree
Hide file tree
Showing 24 changed files with 1,737 additions and 4 deletions.
23 changes: 23 additions & 0 deletions api/prisma/seed-helpers/feature-flag-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Prisma } from '@prisma/client';
import { randomBoolean } from './boolean-generator';
import { randomAdjective, randomName } from './word-generator';

export const featureFlagFactory = (
name = randomName(),
active = randomBoolean(),
description = `${randomAdjective()} feature flag`,
jurisdictionIds?: string[],
): Prisma.FeatureFlagsCreateInput => ({
name: name,
description: description,
active: active,
jurisdictions: jurisdictionIds
? {
connect: jurisdictionIds.map((jurisdiction) => {
return {
id: jurisdiction,
};
}),
}
: undefined,
});
110 changes: 110 additions & 0 deletions api/src/controllers/feature-flag.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { FeatureFlagService } from '../services/feature-flag.service';
import { FeatureFlag } from '../dtos/feature-flags/feature-flag.dto';
import { FeatureFlagAssociate } from '../dtos/feature-flags/feature-flag-associate.dto';
import { FeatureFlagCreate } from '../dtos/feature-flags/feature-flag-create.dto';
import { FeatureFlagUpdate } from '../dtos/feature-flags/feature-flag-update.dto';
import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options';
import { IdDTO } from '../dtos/shared/id.dto';
import { SuccessDTO } from '../dtos/shared/success.dto';
import { PermissionTypeDecorator } from '../decorators/permission-type.decorator';
import { OptionalAuthGuard } from '../guards/optional.guard';
import { PermissionGuard } from '../guards/permission.guard';
import { ApiKeyGuard } from '../guards/api-key.guard';

@Controller('featureFlags')
@ApiTags('featureFlags')
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiExtraModels(
FeatureFlagAssociate,
FeatureFlagCreate,
FeatureFlagUpdate,
IdDTO,
)
@PermissionTypeDecorator('featureFlags')
@UseGuards(ApiKeyGuard, OptionalAuthGuard, PermissionGuard)
export class FeatureFlagController {
constructor(private readonly featureFlagService: FeatureFlagService) {}

@Get()
@ApiOperation({ summary: 'List of feature flags', operationId: 'list' })
@ApiOkResponse({ type: FeatureFlag, isArray: true })
async list(): Promise<FeatureFlag[]> {
return await this.featureFlagService.list();
}

@Post()
@ApiOperation({
summary: 'Create a feature flag',
operationId: 'create',
})
@ApiOkResponse({ type: FeatureFlag })
async create(@Body() featureFlag: FeatureFlagCreate): Promise<FeatureFlag> {
return await this.featureFlagService.create(featureFlag);
}

@Put()
@ApiOperation({
summary: 'Update a feature flag',
operationId: 'update',
})
@ApiOkResponse({ type: FeatureFlag })
async update(@Body() featureFlag: FeatureFlagUpdate): Promise<FeatureFlag> {
return await this.featureFlagService.update(featureFlag);
}

@Delete()
@ApiOperation({
summary: 'Delete a feature flag by id',
operationId: 'delete',
})
@ApiOkResponse({ type: SuccessDTO })
async delete(@Body() dto: IdDTO): Promise<SuccessDTO> {
return await this.featureFlagService.delete(dto.id);
}

@Put(`associateJurisdictions`)
@ApiOperation({
summary: 'Associate and disassociate jurisdictions with a feature flag',
operationId: 'associateJurisdictions',
})
@ApiOkResponse({ type: FeatureFlag })
async associateJurisdictions(
@Body() featureFlagAssociate: FeatureFlagAssociate,
): Promise<FeatureFlag> {
return await this.featureFlagService.associateJurisdictions(
featureFlagAssociate,
);
}

@Get(`:featureFlagId`)
@ApiOperation({
summary: 'Get a feature flag by id',
operationId: 'retrieve',
})
@ApiOkResponse({ type: FeatureFlag })
async retrieve(
@Param('featureFlagId', new ParseUUIDPipe({ version: '4' }))
featureFlagId: string,
): Promise<FeatureFlag> {
return this.featureFlagService.findOne(featureFlagId);
}
}
31 changes: 31 additions & 0 deletions api/src/dtos/feature-flags/feature-flag-associate.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Expose } from 'class-transformer';
import { IsArray, IsDefined, IsString, IsUUID } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class FeatureFlagAssociate {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
id: string;

@Expose()
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, {
groups: [ValidationsGroupsEnum.default],
each: true,
})
@ApiProperty()
associate: string[];

@Expose()
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, {
groups: [ValidationsGroupsEnum.default],
each: true,
})
@ApiProperty()
remove: string[];
}
4 changes: 4 additions & 0 deletions api/src/dtos/feature-flags/feature-flag-create.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { OmitType } from '@nestjs/swagger';
import { FeatureFlagUpdate } from './feature-flag-update.dto';

export class FeatureFlagCreate extends OmitType(FeatureFlagUpdate, ['id']) {}
8 changes: 8 additions & 0 deletions api/src/dtos/feature-flags/feature-flag-update.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OmitType } from '@nestjs/swagger';
import { FeatureFlag } from './feature-flag.dto';

export class FeatureFlagUpdate extends OmitType(FeatureFlag, [
'createdAt',
'updatedAt',
'jurisdictions',
]) {}
39 changes: 39 additions & 0 deletions api/src/dtos/feature-flags/feature-flag.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Expose, Type } from 'class-transformer';
import {
IsBoolean,
IsDefined,
IsString,
MaxLength,
ValidateNested,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { AbstractDTO } from '../shared/abstract.dto';
import { IdDTO } from '../shared/id.dto';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class FeatureFlag extends AbstractDTO {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(256, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
name: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
description: string;

@Expose()
@IsBoolean({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
active: boolean;

@Expose()
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => IdDTO)
@ApiProperty({ type: IdDTO, isArray: true })
jurisdictions: IdDTO[];
}
1 change: 1 addition & 0 deletions api/src/dtos/jurisdictions/jurisdiction-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { Jurisdiction } from './jurisdiction.dto';
export class JurisdictionUpdate extends OmitType(Jurisdiction, [
'createdAt',
'updatedAt',
'featureFlags',
'multiselectQuestions',
]) {}
16 changes: 12 additions & 4 deletions api/src/dtos/jurisdictions/jurisdiction.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AbstractDTO } from '../shared/abstract.dto';
import { Expose, Type } from 'class-transformer';
import {
IsString,
MaxLength,
Expand All @@ -9,11 +9,12 @@ import {
ValidateNested,
IsBoolean,
} from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';
import { LanguagesEnum, UserRoleEnum } from '@prisma/client';
import { Expose, Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { LanguagesEnum, UserRoleEnum } from '@prisma/client';
import { FeatureFlag } from '../feature-flags/feature-flag.dto';
import { AbstractDTO } from '../shared/abstract.dto';
import { IdDTO } from '../shared/id.dto';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class Jurisdiction extends AbstractDTO {
@Expose()
Expand Down Expand Up @@ -140,4 +141,11 @@ export class Jurisdiction extends AbstractDTO {
isArray: true,
})
duplicateListingPermissions: UserRoleEnum[];

@Expose()
@ValidateNested({ groups: [ValidationsGroupsEnum.default] })
@Type(() => FeatureFlag)
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty({ type: FeatureFlag, isArray: true })
featureFlags: FeatureFlag[];
}
3 changes: 3 additions & 0 deletions api/src/modules/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottleGuard } from '../guards/throttler.guard';
import { ScirptRunnerModule } from './script-runner.module';
import { LotteryModule } from './lottery.module';
import { FeatureFlagModule } from './feature-flag.module';

@Module({
imports: [
Expand All @@ -42,6 +43,7 @@ import { LotteryModule } from './lottery.module';
MapLayerModule,
ScirptRunnerModule,
LotteryModule,
FeatureFlagModule,
ThrottlerModule.forRoot([
{
ttl: Number(process.env.THROTTLE_TTL),
Expand Down Expand Up @@ -77,6 +79,7 @@ import { LotteryModule } from './lottery.module';
MapLayerModule,
ScirptRunnerModule,
LotteryModule,
FeatureFlagModule,
],
})
export class AppModule {}
14 changes: 14 additions & 0 deletions api/src/modules/feature-flag.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { FeatureFlagController } from '../controllers/feature-flag.controller';
import { FeatureFlagService } from '../services/feature-flag.service';
import { JurisdictionModule } from './jurisdiction.module';
import { PermissionModule } from './permission.module';
import { PrismaModule } from './prisma.module';

@Module({
imports: [JurisdictionModule, PermissionModule, PrismaModule],
controllers: [FeatureFlagController],
providers: [FeatureFlagService],
exports: [FeatureFlagService],
})
export class FeatureFlagModule {}
2 changes: 2 additions & 0 deletions api/src/permission-configs/permission_policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ p, partner, paperApplication, true, read
p, admin, mapLayers, true, .*
p, jurisdictionAdmin, mapLayers, true, .*

p, admin, featureFlags, true, .*

g, admin, jurisdictionAdmin
g, jurisdictionAdmin, partner
g, partner, user
Expand Down
Loading

0 comments on commit 5261ae8

Please sign in to comment.