Skip to content

Commit 8e31ea7

Browse files
committed
add feature flag service with CRUD endpoints in db-service and api-gateway
1 parent c69ad0e commit 8e31ea7

34 files changed

Lines changed: 1009 additions & 38 deletions

‎packages/api-gateway/src/app.module.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FileConfigModule } from './modules/file_config/file_config.module';
1212
import { AnalyticsModule } from './modules/analytics/analytics.module';
1313
import { AnalyticsConfigModule } from './modules/analytics_config/analytics_config.module';
1414
import { FileDeleteModule } from './modules/file_delete/file_delete.module';
15+
import { FeatureFlagModule } from './modules/feature_flag/feature_flag.module';
1516

1617
@Module({
1718
imports: [
@@ -28,6 +29,7 @@ import { FileDeleteModule } from './modules/file_delete/file_delete.module';
2829
FileDeleteModule,
2930
AnalyticsModule,
3031
AnalyticsConfigModule,
32+
FeatureFlagModule,
3133
],
3234
})
3335
export class AppModule {}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsString, IsOptional, IsBoolean } from 'class-validator';
3+
import { FeatureFlagProto } from 'juno-proto';
4+
5+
export class CreateFeatureFlagModel {
6+
@ApiProperty({
7+
type: 'string',
8+
description: 'Unique identifier for the feature flag',
9+
})
10+
@IsString()
11+
id: string;
12+
13+
@ApiPropertyOptional({
14+
type: 'boolean',
15+
description: 'Whether the flag is enabled. Defaults to false.',
16+
})
17+
@IsOptional()
18+
@IsBoolean()
19+
enabled?: boolean;
20+
21+
@ApiPropertyOptional({
22+
type: 'string',
23+
description: 'Human-readable description of the flag',
24+
})
25+
@IsOptional()
26+
@IsString()
27+
description?: string;
28+
}
29+
30+
export class UpdateFeatureFlagModel {
31+
@ApiPropertyOptional({
32+
type: 'boolean',
33+
description: 'Whether the flag is enabled',
34+
})
35+
@IsOptional()
36+
@IsBoolean()
37+
enabled?: boolean;
38+
39+
@ApiPropertyOptional({
40+
type: 'string',
41+
description: 'Human-readable description of the flag',
42+
})
43+
@IsOptional()
44+
@IsString()
45+
description?: string;
46+
}
47+
48+
export class FeatureFlagResponse {
49+
@ApiProperty({ type: 'string', description: 'Feature flag ID' })
50+
id: string;
51+
52+
@ApiProperty({ type: 'boolean', description: 'Whether the flag is enabled' })
53+
enabled: boolean;
54+
55+
@ApiPropertyOptional({ type: 'string', description: 'Flag description' })
56+
description?: string;
57+
58+
constructor(flag: FeatureFlagProto.FeatureFlag) {
59+
this.id = flag.id;
60+
this.enabled = flag.enabled;
61+
this.description = flag.description;
62+
}
63+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Put,
6+
Delete,
7+
Inject,
8+
OnModuleInit,
9+
Param,
10+
Body,
11+
} from '@nestjs/common';
12+
import { ClientGrpc } from '@nestjs/microservices';
13+
import {
14+
ApiBearerAuth,
15+
ApiBadRequestResponse,
16+
ApiCreatedResponse,
17+
ApiNotFoundResponse,
18+
ApiOkResponse,
19+
ApiOperation,
20+
ApiTags,
21+
ApiUnauthorizedResponse,
22+
} from '@nestjs/swagger';
23+
import { FeatureFlagProto } from 'juno-proto';
24+
import { lastValueFrom } from 'rxjs';
25+
import {
26+
CreateFeatureFlagModel,
27+
UpdateFeatureFlagModel,
28+
FeatureFlagResponse,
29+
} from 'src/models/feature_flag.dto';
30+
31+
const { FEATURE_FLAG_SERVICE_NAME } = FeatureFlagProto;
32+
33+
@ApiBearerAuth('API_Key')
34+
@ApiTags('feature-flag')
35+
@Controller('feature-flags')
36+
export class FeatureFlagController implements OnModuleInit {
37+
private featureFlagService: FeatureFlagProto.FeatureFlagServiceClient;
38+
39+
constructor(
40+
@Inject(FEATURE_FLAG_SERVICE_NAME) private featureFlagClient: ClientGrpc,
41+
) {}
42+
43+
onModuleInit() {
44+
this.featureFlagService =
45+
this.featureFlagClient.getService<FeatureFlagProto.FeatureFlagServiceClient>(
46+
FEATURE_FLAG_SERVICE_NAME,
47+
);
48+
}
49+
50+
@Post()
51+
@ApiOperation({ summary: 'Create a feature flag' })
52+
@ApiCreatedResponse({
53+
description: 'Feature flag created successfully',
54+
type: FeatureFlagResponse,
55+
})
56+
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
57+
@ApiBadRequestResponse({ description: 'Bad Request' })
58+
async createFlag(
59+
@Body() request: CreateFeatureFlagModel,
60+
): Promise<FeatureFlagResponse> {
61+
const response = await lastValueFrom(
62+
this.featureFlagService.createFlag({
63+
id: request.id,
64+
enabled: request.enabled,
65+
description: request.description,
66+
}),
67+
);
68+
return new FeatureFlagResponse(response.flag);
69+
}
70+
71+
@Get(':id')
72+
@ApiOperation({ summary: 'Get a feature flag by ID' })
73+
@ApiOkResponse({
74+
description: 'Feature flag retrieved successfully',
75+
type: FeatureFlagResponse,
76+
})
77+
@ApiNotFoundResponse({ description: 'Feature flag not found' })
78+
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
79+
async getFlag(@Param('id') id: string): Promise<FeatureFlagResponse> {
80+
const response = await lastValueFrom(
81+
this.featureFlagService.getFlag({ id }),
82+
);
83+
return new FeatureFlagResponse(response.flag);
84+
}
85+
86+
@Put(':id')
87+
@ApiOperation({ summary: 'Update a feature flag' })
88+
@ApiOkResponse({
89+
description: 'Feature flag updated successfully',
90+
type: FeatureFlagResponse,
91+
})
92+
@ApiNotFoundResponse({ description: 'Feature flag not found' })
93+
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
94+
@ApiBadRequestResponse({ description: 'Bad Request' })
95+
async setFlag(
96+
@Param('id') id: string,
97+
@Body() request: UpdateFeatureFlagModel,
98+
): Promise<FeatureFlagResponse> {
99+
const response = await lastValueFrom(
100+
this.featureFlagService.setFlag({
101+
id,
102+
enabled: request.enabled,
103+
description: request.description,
104+
}),
105+
);
106+
return new FeatureFlagResponse(response.flag);
107+
}
108+
109+
@Delete(':id')
110+
@ApiOperation({ summary: 'Delete a feature flag' })
111+
@ApiOkResponse({ description: 'Feature flag deleted successfully' })
112+
@ApiNotFoundResponse({ description: 'Feature flag not found' })
113+
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
114+
async deleteFlag(@Param('id') id: string): Promise<{ success: boolean }> {
115+
const response = await lastValueFrom(
116+
this.featureFlagService.deleteFlag({ id }),
117+
);
118+
return { success: response.success };
119+
}
120+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { ClientsModule, Transport } from '@nestjs/microservices';
4+
import { join } from 'path';
5+
import { FeatureFlagController } from './feature_flag.controller';
6+
import {
7+
ApiKeyProto,
8+
ApiKeyProtoFile,
9+
FeatureFlagProto,
10+
FeatureFlagProtoFile,
11+
JwtProto,
12+
JwtProtoFile,
13+
} from 'juno-proto';
14+
import { ApiKeyMiddleware } from 'src/middleware/api_key.middleware';
15+
16+
const { JWT_SERVICE_NAME, JUNO_JWT_PACKAGE_NAME } = JwtProto;
17+
const { API_KEY_SERVICE_NAME, JUNO_API_KEY_PACKAGE_NAME } = ApiKeyProto;
18+
const { FEATURE_FLAG_SERVICE_NAME, JUNO_FEATURE_FLAG_PACKAGE_NAME } =
19+
FeatureFlagProto;
20+
21+
@Module({
22+
imports: [
23+
ConfigModule.forRoot({
24+
envFilePath: join(__dirname, '../../../../../.env.local'),
25+
}),
26+
ClientsModule.register([
27+
{
28+
name: JWT_SERVICE_NAME,
29+
transport: Transport.GRPC,
30+
options: {
31+
url: process.env.AUTH_SERVICE_ADDR,
32+
package: JUNO_JWT_PACKAGE_NAME,
33+
protoPath: JwtProtoFile,
34+
},
35+
},
36+
{
37+
name: API_KEY_SERVICE_NAME,
38+
transport: Transport.GRPC,
39+
options: {
40+
url: process.env.AUTH_SERVICE_ADDR,
41+
package: JUNO_API_KEY_PACKAGE_NAME,
42+
protoPath: ApiKeyProtoFile,
43+
},
44+
},
45+
{
46+
name: FEATURE_FLAG_SERVICE_NAME,
47+
transport: Transport.GRPC,
48+
options: {
49+
url: process.env.DB_SERVICE_ADDR,
50+
package: JUNO_FEATURE_FLAG_PACKAGE_NAME,
51+
protoPath: FeatureFlagProtoFile,
52+
},
53+
},
54+
]),
55+
],
56+
controllers: [FeatureFlagController],
57+
})
58+
export class FeatureFlagModule implements NestModule {
59+
configure(consumer: MiddlewareConsumer) {
60+
consumer.apply(ApiKeyMiddleware).forRoutes('feature-flags*');
61+
}
62+
}

0 commit comments

Comments
 (0)