diff --git a/packages/api-gateway/src/app.module.ts b/packages/api-gateway/src/app.module.ts index afd72ce7..035c0c7c 100644 --- a/packages/api-gateway/src/app.module.ts +++ b/packages/api-gateway/src/app.module.ts @@ -12,6 +12,7 @@ import { FileConfigModule } from './modules/file_config/file_config.module'; import { AnalyticsModule } from './modules/analytics/analytics.module'; import { AnalyticsConfigModule } from './modules/analytics_config/analytics_config.module'; import { FileDeleteModule } from './modules/file_delete/file_delete.module'; +import { FeatureFlagModule } from './modules/feature_flag/feature_flag.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { FileDeleteModule } from './modules/file_delete/file_delete.module'; FileDeleteModule, AnalyticsModule, AnalyticsConfigModule, + FeatureFlagModule, ], }) export class AppModule {} diff --git a/packages/api-gateway/src/models/feature_flag.dto.ts b/packages/api-gateway/src/models/feature_flag.dto.ts new file mode 100644 index 00000000..c282cf38 --- /dev/null +++ b/packages/api-gateway/src/models/feature_flag.dto.ts @@ -0,0 +1,40 @@ +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateFeatureFlagModel { + @IsString() + @IsNotEmpty() + id: string; + + @IsBoolean() + enabled: boolean; + + @IsOptional() + @IsString() + description?: string; +} + +export class UpdateFeatureFlagModel { + @IsOptional() + @IsBoolean() + enabled?: boolean; + + @IsOptional() + @IsString() + description?: string; +} + +export class FeatureFlagResponse { + id: string; + enabled: boolean; + description?: string; + + constructor(flag: { + id: string; + enabled: boolean; + description?: string | null; + }) { + this.id = flag.id; + this.enabled = flag.enabled; + this.description = flag.description ?? undefined; + } +} diff --git a/packages/api-gateway/src/modules/feature_flag/feature_flag.controller.ts b/packages/api-gateway/src/modules/feature_flag/feature_flag.controller.ts new file mode 100644 index 00000000..91f10973 --- /dev/null +++ b/packages/api-gateway/src/modules/feature_flag/feature_flag.controller.ts @@ -0,0 +1,106 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Inject, + OnModuleInit, + Param, + Post, + Put, +} from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { FeatureFlagProto } from 'juno-proto'; +import { lastValueFrom } from 'rxjs'; +import { + CreateFeatureFlagModel, + FeatureFlagResponse, + UpdateFeatureFlagModel, +} from 'src/models/feature_flag.dto'; + +const { FEATURE_FLAG_SERVICE_NAME } = FeatureFlagProto; + +@Controller('feature_flag') +@ApiBearerAuth('API_Key') +@ApiTags('feature_flag') +export class FeatureFlagController implements OnModuleInit { + private featureFlagService: FeatureFlagProto.FeatureFlagServiceClient; + + constructor( + @Inject(FEATURE_FLAG_SERVICE_NAME) + private featureFlagClient: ClientGrpc, + ) {} + + onModuleInit() { + this.featureFlagService = + this.featureFlagClient.getService( + FEATURE_FLAG_SERVICE_NAME, + ); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a feature flag. ' }) + @ApiResponse({ + status: HttpStatus.OK, + type: FeatureFlagResponse, + }) + async getFlag(@Param('id') id: string): Promise { + const flag = await lastValueFrom(this.featureFlagService.getFlag({ id })); + return new FeatureFlagResponse(flag); + } + + @Post() + @ApiOperation({ summary: 'Create a feature flag' }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: FeatureFlagResponse, + }) + async createFlag( + @Body() request: CreateFeatureFlagModel, + ): Promise { + const flag = await lastValueFrom( + this.featureFlagService.createFlag(request), + ); + return new FeatureFlagResponse(flag); + } + + @Put(':id') + @ApiOperation({ summary: 'Update a feature flag' }) + @ApiResponse({ + status: HttpStatus.OK, + type: FeatureFlagResponse, + }) + async setFlag( + @Param('id') id: string, + @Body() request: UpdateFeatureFlagModel, + ): Promise { + const flag = await lastValueFrom( + this.featureFlagService.setFlag({ + id, + enabled: request.enabled, + description: request.description, + }), + ); + return new FeatureFlagResponse(flag); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a feature flag' }) + @ApiResponse({ + status: HttpStatus.OK, + type: FeatureFlagResponse, + }) + async deleteFlag(@Param('id') id: string) { + const flag = await lastValueFrom( + this.featureFlagService.deleteFlag({ id }), + ); + return new FeatureFlagResponse(flag); + } +} diff --git a/packages/api-gateway/src/modules/feature_flag/feature_flag.module.ts b/packages/api-gateway/src/modules/feature_flag/feature_flag.module.ts new file mode 100644 index 00000000..98abe2ec --- /dev/null +++ b/packages/api-gateway/src/modules/feature_flag/feature_flag.module.ts @@ -0,0 +1,60 @@ +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { join } from 'path'; +import { + ApiKeyProto, + ApiKeyProtoFile, + FeatureFlagProto, + FeatureFlagProtoFile, + JwtProto, + JwtProtoFile, +} from 'juno-proto'; +import { ApiKeyMiddleware } from 'src/middleware/api_key.middleware'; +import { FeatureFlagController } from './feature_flag.controller'; + +const { API_KEY_SERVICE_NAME, JUNO_API_KEY_PACKAGE_NAME } = ApiKeyProto; +const { JWT_SERVICE_NAME, JUNO_JWT_PACKAGE_NAME } = JwtProto; + +@Module({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(__dirname, '../../../../../.env.local'), + }), + ClientsModule.register([ + { + name: API_KEY_SERVICE_NAME, + transport: Transport.GRPC, + options: { + url: process.env.AUTH_SERVICE_ADDR, + package: JUNO_API_KEY_PACKAGE_NAME, + protoPath: ApiKeyProtoFile, + }, + }, + { + name: JWT_SERVICE_NAME, + transport: Transport.GRPC, + options: { + url: process.env.AUTH_SERVICE_ADDR, + package: JUNO_JWT_PACKAGE_NAME, + protoPath: JwtProtoFile, + }, + }, + { + name: FeatureFlagProto.FEATURE_FLAG_SERVICE_NAME, + transport: Transport.GRPC, + options: { + url: process.env.DB_SERVICE_ADDR, + package: FeatureFlagProto.JUNO_FEATURE_FLAG_PACKAGE_NAME, + protoPath: FeatureFlagProtoFile, + }, + }, + ]), + ], + controllers: [FeatureFlagController], +}) +export class FeatureFlagModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(ApiKeyMiddleware).forRoutes('feature_flag*'); + } +} diff --git a/packages/api-gateway/test/feature_flag.e2e-spec.ts b/packages/api-gateway/test/feature_flag.e2e-spec.ts new file mode 100644 index 00000000..6e00179c --- /dev/null +++ b/packages/api-gateway/test/feature_flag.e2e-spec.ts @@ -0,0 +1,166 @@ +import { + ClassSerializerInterceptor, + INestApplication, + ValidationPipe, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as request from 'supertest'; +import { Reflector } from '@nestjs/core'; +import * as GRPC from '@grpc/grpc-js'; +import * as ProtoLoader from '@grpc/proto-loader'; +import { ResetProtoFile } from 'juno-proto'; +import { AppModule } from '../src/app.module'; +import { RpcExceptionFilter } from 'src/rpc_exception_filter'; + +let app: INestApplication; +let apiKey: string; + +const ADMIN_EMAIL = 'test-superadmin@test.com'; +const ADMIN_PASSWORD = 'test-password'; + +jest.setTimeout(15000); + +async function createApiKey(): Promise { + const response = await request(app.getHttpServer()) + .post('/auth/key') + .set('X-User-Email', ADMIN_EMAIL) + .set('X-User-Password', ADMIN_PASSWORD) + .send({ + environment: 'prod', + project: { name: 'test-seed-project' }, + }); + + return response.body.apiKey; +} + +beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + }), + ); + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + app.useGlobalFilters(new RpcExceptionFilter()); + + await app.init(); + + const proto = ProtoLoader.loadSync([ResetProtoFile]) as any; + const protoGrpc = GRPC.loadPackageDefinition(proto) as any; + const resetClient = new protoGrpc.juno.reset_db.DatabaseReset( + process.env.DB_SERVICE_ADDR, + GRPC.credentials.createInsecure(), + ); + + await new Promise((resolve, reject) => { + resetClient.resetDb({}, (error: Error | null) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +}); + +beforeEach(async () => { + if (!apiKey) { + apiKey = await createApiKey(); + } +}); + +afterAll(async () => { + await app.close(); +}); + +describe('Feature Flag Routes (e2e)', () => { + it('creates and retrieves a feature flag', async () => { + await request(app.getHttpServer()) + .post('/feature_flag') + .set('Authorization', `Bearer ${apiKey}`) + .send({ + id: 'new-flag', + enabled: false, + description: 'temp', + }) + .expect(201) + .expect((response) => { + expect(response.body).toEqual({ + id: 'new-flag', + enabled: false, + description: 'temp', + }); + }); + + await request(app.getHttpServer()) + .get('/feature_flag/new-flag') + .set('Authorization', `Bearer ${apiKey}`) + .expect(200) + .expect((response) => { + expect(response.body).toEqual({ + id: 'new-flag', + enabled: false, + description: 'temp', + }); + }); + }); + + it('rejects an invalid create request', async () => { + await request(app.getHttpServer()) + .post('/feature_flag') + .set('Authorization', `Bearer ${apiKey}`) + .send({ enabled: true }) + .expect(400); + }); + + it('updates only the fields supplied', async () => { + await request(app.getHttpServer()) + .post('/feature_flag') + .set('Authorization', `Bearer ${apiKey}`) + .send({ + id: 'updated-flag', + enabled: false, + description: 'original', + }) + .expect(201); + + await request(app.getHttpServer()) + .put('/feature_flag/updated-flag') + .set('Authorization', `Bearer ${apiKey}`) + .send({ enabled: true }) + .expect(200) + .expect((response) => { + expect(response.body).toEqual({ + id: 'updated-flag', + enabled: true, + description: 'original', + }); + }); + }); + + it('deletes a feature flag and returns not found afterward', async () => { + await request(app.getHttpServer()) + .post('/feature_flag') + .set('Authorization', `Bearer ${apiKey}`) + .send({ + id: 'deleted-flag', + enabled: true, + description: 'temp', + }) + .expect(201); + + await request(app.getHttpServer()) + .delete('/feature_flag/deleted-flag') + .set('Authorization', `Bearer ${apiKey}`) + .expect(200); + + await request(app.getHttpServer()) + .get('/feature_flag/deleted-flag') + .set('Authorization', `Bearer ${apiKey}`) + .expect(404); + }); +}); diff --git a/packages/db-service/prisma/migrations/20260909012259_add_feature_flag/migration.sql b/packages/db-service/prisma/migrations/20260909012259_add_feature_flag/migration.sql new file mode 100644 index 00000000..1106f368 --- /dev/null +++ b/packages/db-service/prisma/migrations/20260909012259_add_feature_flag/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "FeatureFlag" ( + "id" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT false, + "description" TEXT, + + CONSTRAINT "FeatureFlag_pkey" PRIMARY KEY ("id") +); diff --git a/packages/db-service/prisma/schema.prisma b/packages/db-service/prisma/schema.prisma index ada0428b..998b7bf8 100644 --- a/packages/db-service/prisma/schema.prisma +++ b/packages/db-service/prisma/schema.prisma @@ -165,3 +165,9 @@ model NewAccountRequest { projectName String? createdAt DateTime @default(now()) } + +model FeatureFlag { + id String @id + enabled Boolean @default(false) + description String? +} \ No newline at end of file diff --git a/packages/db-service/src/app.module.ts b/packages/db-service/src/app.module.ts index 6ce22f40..55cb0939 100644 --- a/packages/db-service/src/app.module.ts +++ b/packages/db-service/src/app.module.ts @@ -13,6 +13,7 @@ import { FileBucketModule } from './modules/file_bucket/file_bucket.module'; import { FileConfigModule } from './modules/file_config/file_config.module'; import { FileProviderModule } from './modules/file_provider/file_provider.module'; import { AnalyticsConfigModule } from './modules/analytics_config/analytics_config.module'; +import { FeatureFlagModule } from './modules/feature_flag/feature_flag.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { AnalyticsConfigModule } from './modules/analytics_config/analytics_conf FileConfigModule, FileProviderModule, AnalyticsConfigModule, + FeatureFlagModule, ], controllers: [AppController], providers: [AppService], diff --git a/packages/db-service/src/main.ts b/packages/db-service/src/main.ts index 3b2b0c5a..ad97f6c1 100644 --- a/packages/db-service/src/main.ts +++ b/packages/db-service/src/main.ts @@ -28,6 +28,8 @@ import { FileProviderProtoFile, AnalyticsConfigProto, AnalyticsConfigProtoFile, + FeatureFlagProto, + FeatureFlagProtoFile, } from 'juno-proto'; import { CustomRpcExceptionFilter } from './global-exception.filter'; @@ -52,6 +54,7 @@ async function bootstrap() { FileConfigProto.JUNO_FILE_SERVICE_CONFIG_PACKAGE_NAME, FileProviderProto.JUNO_FILE_SERVICE_PROVIDER_PACKAGE_NAME, AnalyticsConfigProto.JUNO_ANALYTICS_SERVICE_ANALYTICS_CONFIG_PACKAGE_NAME, + FeatureFlagProto.JUNO_FEATURE_FLAG_PACKAGE_NAME, ], protoPath: [ UserProtoFile, @@ -66,6 +69,7 @@ async function bootstrap() { FileConfigProtoFile, FileProviderProtoFile, AnalyticsConfigProtoFile, + FeatureFlagProtoFile, ], url: process.env.DB_SERVICE_ADDR, }, diff --git a/packages/db-service/src/modules/feature_flag/feature_flag.controller.ts b/packages/db-service/src/modules/feature_flag/feature_flag.controller.ts new file mode 100644 index 00000000..b3b7d50b --- /dev/null +++ b/packages/db-service/src/modules/feature_flag/feature_flag.controller.ts @@ -0,0 +1,70 @@ +import { Controller } from '@nestjs/common'; +import { FeatureFlagProto } from 'juno-proto'; +import { RpcException } from '@nestjs/microservices'; +import { status } from '@grpc/grpc-js'; +import { FeatureFlagService } from './feature_flag.service'; +import { Prisma } from '@prisma/client'; + +@Controller() +@FeatureFlagProto.FeatureFlagServiceControllerMethods() +export class FeatureFlagController + implements FeatureFlagProto.FeatureFlagServiceController +{ + constructor(private readonly featureFlagService: FeatureFlagService) {} + + async createFlag( + request: FeatureFlagProto.CreateFlagRequest, + ): Promise { + try { + const flag = await this.featureFlagService.createFlag({ + id: request.id, + enabled: request.enabled, + description: request.description, + }); + return flag; + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new RpcException({ + code: status.ALREADY_EXISTS, + message: 'Feature flag with this id already exists', + }); + } + throw error; + } + } + + async getFlag( + request: FeatureFlagProto.GetFlagRequest, + ): Promise { + const flag = await this.featureFlagService.getFlag(request.id); + + if (!flag) { + throw new RpcException({ + code: status.NOT_FOUND, + message: 'Flag not found', + }); + } + + return flag; + } + + async setFlag( + request: FeatureFlagProto.SetFlagRequest, + ): Promise { + const flag = await this.featureFlagService.setFlag(request.id, { + enabled: request.enabled, + description: request.description, + }); + return flag; + } + + async deleteFlag( + request: FeatureFlagProto.DeleteFlagRequest, + ): Promise { + const flag = await this.featureFlagService.deleteFlag(request.id); + return flag; + } +} diff --git a/packages/db-service/src/modules/feature_flag/feature_flag.module.ts b/packages/db-service/src/modules/feature_flag/feature_flag.module.ts new file mode 100644 index 00000000..c9770d7d --- /dev/null +++ b/packages/db-service/src/modules/feature_flag/feature_flag.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from 'src/prisma.service'; +import { FeatureFlagService } from './feature_flag.service'; +import { FeatureFlagController } from './feature_flag.controller'; + +@Module({ + providers: [FeatureFlagService, PrismaService], + controllers: [FeatureFlagController], +}) +export class FeatureFlagModule {} diff --git a/packages/db-service/src/modules/feature_flag/feature_flag.service.ts b/packages/db-service/src/modules/feature_flag/feature_flag.service.ts new file mode 100644 index 00000000..b2526fc3 --- /dev/null +++ b/packages/db-service/src/modules/feature_flag/feature_flag.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, FeatureFlag } from '@prisma/client'; +import { PrismaService } from 'src/prisma.service'; + +type FeatureFlagUpdateData = { + enabled?: boolean; + description?: string | null; +}; + +@Injectable() +export class FeatureFlagService { + constructor(private prisma: PrismaService) {} + + async createFlag(input: Prisma.FeatureFlagCreateInput): Promise { + return this.prisma.featureFlag.create({ + data: input, + }); + } + + async getFlag(id: string): Promise { + return this.prisma.featureFlag.findUnique({ where: { id } }); + } + + async setFlag(id: string, data: FeatureFlagUpdateData): Promise { + return this.prisma.featureFlag.update({ + where: { id }, + data, + }); + } + + async deleteFlag(id: string): Promise { + return this.prisma.featureFlag.delete({ where: { id } }); + } +} diff --git a/packages/db-service/test/feature_flag.e2e-spec.ts b/packages/db-service/test/feature_flag.e2e-spec.ts new file mode 100644 index 00000000..d1610547 --- /dev/null +++ b/packages/db-service/test/feature_flag.e2e-spec.ts @@ -0,0 +1,198 @@ +import * as GRPC from '@grpc/grpc-js'; +import * as ProtoLoader from '@grpc/proto-loader'; +import { INestMicroservice } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import { + FeatureFlagProto, + FeatureFlagProtoFile, + ResetProto, + ResetProtoFile, +} from 'juno-proto'; +import { AppModule } from 'src/app.module'; + +let app: INestMicroservice; + +jest.setTimeout(15000); + +async function initApp() { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + const microservice = + moduleFixture.createNestMicroservice({ + transport: Transport.GRPC, + options: { + package: [ + FeatureFlagProto.JUNO_FEATURE_FLAG_PACKAGE_NAME, + ResetProto.JUNO_RESET_DB_PACKAGE_NAME, + ], + protoPath: [FeatureFlagProtoFile, ResetProtoFile], + url: process.env.DB_SERVICE_ADDR, + }, + }); + + await microservice.init(); + await microservice.listen(); + return microservice; +} + +async function resetDatabase() { + const proto = ProtoLoader.loadSync([ResetProtoFile]) as any; + const protoGrpc = GRPC.loadPackageDefinition(proto) as any; + const resetClient = new protoGrpc.juno.reset_db.DatabaseReset( + process.env.DB_SERVICE_ADDR, + GRPC.credentials.createInsecure(), + ); + + await new Promise((resolve, reject) => { + resetClient.resetDb({}, (error: Error | null) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} + +function createFeatureFlagClient() { + const proto = ProtoLoader.loadSync([FeatureFlagProtoFile]) as any; + const protoGrpc = GRPC.loadPackageDefinition(proto) as any; + return new protoGrpc.juno.feature_flag.FeatureFlagService( + process.env.DB_SERVICE_ADDR, + GRPC.credentials.createInsecure(), + ); +} + +function callGrpc( + call: (callback: (error: any, response: T) => void) => void, +) { + return new Promise((resolve, reject) => { + call((error, response) => { + if (error) { + reject(error); + } else { + resolve(response); + } + }); + }); +} + +beforeAll(async () => { + app = await initApp(); + await resetDatabase(); + await app.close(); +}); + +beforeEach(async () => { + app = await initApp(); +}); + +afterEach(async () => { + await app.close(); +}); + +describe('Feature Flag Tests', () => { + let featureFlagClient: any; + + beforeEach(() => { + featureFlagClient = createFeatureFlagClient(); + }); + + it('creates and retrieves a feature flag', async () => { + const created = await callGrpc((callback) => + featureFlagClient.createFlag( + { + id: 'dark-mode', + enabled: false, + description: 'Controls dark mode', + }, + callback, + ), + ); + + expect(created).toEqual({ + id: 'dark-mode', + enabled: false, + description: 'Controls dark mode', + }); + + const retrieved = await callGrpc((callback) => + featureFlagClient.getFlag({ id: 'dark-mode' }, callback), + ); + + expect(retrieved).toEqual(created); + }); + + it('rejects duplicate feature flag IDs', async () => { + const flag = { id: 'duplicate', enabled: false, description: 'first' }; + + await callGrpc((callback) => + featureFlagClient.createFlag(flag, callback), + ); + + await expect( + callGrpc((callback) => + featureFlagClient.createFlag(flag, callback), + ), + ).rejects.toMatchObject({ code: GRPC.status.ALREADY_EXISTS }); + }); + + it('updates only the fields provided', async () => { + await callGrpc((callback) => + featureFlagClient.createFlag( + { + id: 'partial-update', + enabled: false, + description: 'original', + }, + callback, + ), + ); + + const updated = await callGrpc((callback) => + featureFlagClient.setFlag( + { id: 'partial-update', enabled: true }, + callback, + ), + ); + + expect(updated).toEqual({ + id: 'partial-update', + enabled: true, + description: 'original', + }); + }); + + it('deletes a feature flag', async () => { + await callGrpc((callback) => + featureFlagClient.createFlag( + { id: 'to-delete', enabled: true, description: 'temporary' }, + callback, + ), + ); + + const deleted = await callGrpc((callback) => + featureFlagClient.deleteFlag({ id: 'to-delete' }, callback), + ); + + expect(deleted.id).toBe('to-delete'); + + await expect( + callGrpc((callback) => + featureFlagClient.getFlag({ id: 'to-delete' }, callback), + ), + ).rejects.toMatchObject({ code: GRPC.status.NOT_FOUND }); + }); +}); diff --git a/packages/proto/definitions/feature_flag.proto b/packages/proto/definitions/feature_flag.proto new file mode 100644 index 00000000..455fcce5 --- /dev/null +++ b/packages/proto/definitions/feature_flag.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package juno.feature_flag; + +service FeatureFlagService { + rpc createFlag(CreateFlagRequest) returns (FeatureFlag); + rpc getFlag(GetFlagRequest) returns (FeatureFlag); + rpc setFlag(SetFlagRequest) returns (FeatureFlag); + rpc deleteFlag(DeleteFlagRequest) returns (FeatureFlag); +} + +message FeatureFlag { + string id = 1; + bool enabled = 2; + optional string description = 3; +} + +message CreateFlagRequest { + string id = 1; + bool enabled = 2; + optional string description = 3; +} + +message GetFlagRequest { + string id = 1; +} + +message SetFlagRequest { + string id = 1; + optional bool enabled = 2; + optional string description = 3; +} + +message DeleteFlagRequest { + string id = 1; +} \ No newline at end of file diff --git a/packages/proto/src/gen/feature_flag.ts b/packages/proto/src/gen/feature_flag.ts new file mode 100644 index 00000000..f55f95dc --- /dev/null +++ b/packages/proto/src/gen/feature_flag.ts @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v1.181.2 +// protoc v5.29.3 +// source: feature_flag.proto + +/* eslint-disable */ +import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; +import { Observable } from 'rxjs'; + +export const protobufPackage = 'juno.feature_flag'; + +export interface FeatureFlag { + id: string; + enabled: boolean; + description?: string | undefined; +} + +export interface CreateFlagRequest { + id: string; + enabled: boolean; + description?: string | undefined; +} + +export interface GetFlagRequest { + id: string; +} + +export interface SetFlagRequest { + id: string; + enabled?: boolean | undefined; + description?: string | undefined; +} + +export interface DeleteFlagRequest { + id: string; +} + +export const JUNO_FEATURE_FLAG_PACKAGE_NAME = 'juno.feature_flag'; + +export interface FeatureFlagServiceClient { + createFlag(request: CreateFlagRequest): Observable; + + getFlag(request: GetFlagRequest): Observable; + + setFlag(request: SetFlagRequest): Observable; + + deleteFlag(request: DeleteFlagRequest): Observable; +} + +export interface FeatureFlagServiceController { + createFlag( + request: CreateFlagRequest, + ): Promise | Observable | FeatureFlag; + + getFlag( + request: GetFlagRequest, + ): Promise | Observable | FeatureFlag; + + setFlag( + request: SetFlagRequest, + ): Promise | Observable | FeatureFlag; + + deleteFlag( + request: DeleteFlagRequest, + ): Promise | Observable | FeatureFlag; +} + +export function FeatureFlagServiceControllerMethods() { + return function (constructor: Function) { + const grpcMethods: string[] = [ + 'createFlag', + 'getFlag', + 'setFlag', + 'deleteFlag', + ]; + for (const method of grpcMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcMethod('FeatureFlagService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + const grpcStreamMethods: string[] = []; + for (const method of grpcStreamMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcStreamMethod('FeatureFlagService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + }; +} + +export const FEATURE_FLAG_SERVICE_NAME = 'FeatureFlagService'; diff --git a/packages/proto/src/main.ts b/packages/proto/src/main.ts index cb6cf67f..40342a65 100644 --- a/packages/proto/src/main.ts +++ b/packages/proto/src/main.ts @@ -15,6 +15,7 @@ export * as FileProviderProto from './gen/file_provider'; export * as CommonProto from './gen/common'; export * as AnalyticsProto from './gen/analytics'; export * as AnalyticsConfigProto from './gen/analytics_config'; +export * as FeatureFlagProto from './gen/feature_flag'; import { join } from 'path'; @@ -41,3 +42,4 @@ export const AnalyticsProtoFile = getProtoFilePath('analytics.proto'); export const AnalyticsConfigProtoFile = getProtoFilePath( 'analytics_config.proto', ); +export const FeatureFlagProtoFile = getProtoFilePath('feature_flag.proto'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a53801c8..6743c066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3659,6 +3659,7 @@ packages: { integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==, } + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@webassemblyjs/ast@1.12.1': resolution: @@ -5440,7 +5441,7 @@ packages: { integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, } - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: @@ -7613,7 +7614,7 @@ packages: integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==, } engines: { node: '>=6.4.0 <13 || >=14' } - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superagent@9.0.2: resolution: @@ -7707,7 +7708,7 @@ packages: integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==, } engines: { node: '>=10' } - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.10: resolution: @@ -8058,6 +8059,7 @@ packages: { integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==, } + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: