Skip to content

Commit

Permalink
feat(api): add get environment tags endpoint (#6713)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX authored Oct 17, 2024
1 parent b781886 commit 9f2c5a3
Show file tree
Hide file tree
Showing 48 changed files with 174 additions and 18 deletions.
4 changes: 2 additions & 2 deletions apps/api/migrations/changes-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
OrganizationRepository,
} from '@novu/dal';
import { ChangeEntityTypeEnum, MemberRoleEnum } from '@novu/shared';
import { CreateEnvironment } from '../src/app/environments/usecases/create-environment/create-environment.usecase';
import { CreateEnvironmentCommand } from '../src/app/environments/usecases/create-environment/create-environment.command';
import { CreateEnvironment } from '../src/app/environments-v1/usecases/create-environment/create-environment.usecase';
import { CreateEnvironmentCommand } from '../src/app/environments-v1/usecases/create-environment/create-environment.command';
import { ApplyChange } from '../src/app/change/usecases/apply-change/apply-change.usecase';
import { ApplyChangeCommand } from '../src/app/change/usecases/apply-change/apply-change.command';
import { CreateChange, CreateChangeCommand } from '@novu/application-generic';
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { AuthModule } from './app/auth/auth.module';
import { TestingModule } from './app/testing/testing.module';
import { HealthModule } from './app/health/health.module';
import { OrganizationModule } from './app/organization/organization.module';
import { EnvironmentsModule } from './app/environments/environments.module';
import { ExecutionDetailsModule } from './app/execution-details/execution-details.module';
import { EventsModule } from './app/events/events.module';
import { WidgetsModule } from './app/widgets/widgets.module';
Expand Down Expand Up @@ -44,6 +43,8 @@ import { PreferencesModule } from './app/preferences';
import { StepSchemasModule } from './app/step-schemas/step-schemas.module';
import { WorkflowModule } from './app/workflows-v2/workflow.module';
import { WorkflowModuleV1 } from './app/workflows-v1/workflow-v1.module';
import { EnvironmentsModuleV1 } from './app/environments-v1/environments-v1.module';
import { EnvironmentsModule } from './app/environments-v2/environments.module';

const enterpriseImports = (): Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> => {
const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [];
Expand Down Expand Up @@ -76,7 +77,7 @@ const baseModules: Array<Type | DynamicModule | Promise<DynamicModule> | Forward
InboundParseModule,
SharedModule,
HealthModule,
EnvironmentsModule,
EnvironmentsModuleV1,
ExecutionDetailsModule,
WorkflowModuleV1,
EventsModule,
Expand Down Expand Up @@ -106,6 +107,7 @@ const baseModules: Array<Type | DynamicModule | Promise<DynamicModule> | Forward
BridgeModule,
PreferencesModule,
WorkflowModule,
EnvironmentsModule,
];

const enterpriseModules = enterpriseImports();
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/community.auth.module.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { USE_CASES } from './usecases';
import { SharedModule } from '../shared/shared.module';
import { GitHubStrategy } from './services/passport/github.strategy';
import { OrganizationModule } from '../organization/organization.module';
import { EnvironmentsModule } from '../environments/environments.module';
import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module';
import { JwtSubscriberStrategy } from './services/passport/subscriber-jwt.strategy';
import { RootEnvironmentGuard } from './framework/root-environment-guard.service';
import { ApiKeyStrategy } from './services/passport/apikey.strategy';
Expand All @@ -39,7 +39,7 @@ export function getCommunityAuthModuleConfig(): ModuleMetadata {
expiresIn: 360000,
},
}),
EnvironmentsModule,
EnvironmentsModuleV1,
],
controllers: [AuthController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.de
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators';

/**
* @deprecated use EnvironmentsControllerV2
*/
@ApiCommonResponses()
@Controller('/environments')
@UseInterceptors(ClassSerializerInterceptor)
@UserAuthentication()
@ApiTags('Environments')
export class EnvironmentsController {
export class EnvironmentsControllerV1 {
constructor(
private createEnvironmentUsecase: CreateEnvironment,
private updateEnvironmentUsecase: UpdateEnvironment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { forwardRef, Module } from '@nestjs/common';

import { SharedModule } from '../shared/shared.module';
import { USE_CASES } from './usecases';
import { EnvironmentsController } from './environments.controller';
import { EnvironmentsControllerV1 } from './environments-v1.controller';
import { NotificationGroupsModule } from '../notification-groups/notification-groups.module';
import { AuthModule } from '../auth/auth.module';
import { LayoutsModule } from '../layouts/layouts.module';
Expand All @@ -16,8 +16,8 @@ import { NovuBridgeModule } from './novu-bridge.module';
forwardRef(() => LayoutsModule),
NovuBridgeModule,
],
controllers: [EnvironmentsController],
controllers: [EnvironmentsControllerV1],
providers: [...USE_CASES],
exports: [...USE_CASES],
})
export class EnvironmentsModule {}
export class EnvironmentsModuleV1 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsString } from 'class-validator';

export class GetEnvironmentTagsDto {
@ApiProperty()
@IsDefined()
@IsString()
name: string;
}
34 changes: 34 additions & 0 deletions apps/api/src/app/environments-v2/environments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ClassSerializerInterceptor, Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { UserSessionData } from '@novu/shared';
import { ApiTags } from '@nestjs/swagger';
import { UserSession } from '../shared/framework/user.decorator';
import { GetEnvironmentTags, GetEnvironmentTagsCommand } from './usecases/get-environment-tags';
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.decorator';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
import { GetEnvironmentTagsDto } from './dtos/get-environment-tags.dto';

@ApiCommonResponses()
@Controller({ path: `/environments`, version: '2' })
@UseInterceptors(ClassSerializerInterceptor)
@UserAuthentication()
@ApiTags('Environments')
export class EnvironmentsController {
constructor(private getEnvironmentTagsUsecase: GetEnvironmentTags) {}

@Get('/:environmentId/tags')
@ApiResponse(GetEnvironmentTagsDto)
@ExternalApiAccessible()
async getEnvironmentTags(
@UserSession() user: UserSessionData,
@Param('environmentId') environmentId: string
): Promise<GetEnvironmentTagsDto[]> {
return await this.getEnvironmentTagsUsecase.execute(
GetEnvironmentTagsCommand.create({
environmentId,
userId: user._id,
organizationId: user.organizationId,
})
);
}
}
12 changes: 12 additions & 0 deletions apps/api/src/app/environments-v2/environments.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { EnvironmentsController } from './environments.controller';
import { GetEnvironmentTags } from './usecases/get-environment-tags';
import { SharedModule } from '../shared/shared.module';

@Module({
imports: [SharedModule],
controllers: [EnvironmentsController],
providers: [GetEnvironmentTags],
exports: [],
})
export class EnvironmentsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';

export class GetEnvironmentTagsCommand extends EnvironmentWithUserCommand {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';

describe('Get Environment Tags - /v2/environments/:environmentId/tags (GET)', async () => {
let session: UserSession;
const environmentRepository = new EnvironmentRepository();
const notificationTemplateRepository = new NotificationTemplateRepository();

before(async () => {
session = new UserSession();
await session.initialize();
});

it('should return correct tags for the environment', async () => {
await notificationTemplateRepository.create({
_environmentId: session.environment._id,
tags: ['tag1', 'tag2'],
});
await notificationTemplateRepository.create({
_environmentId: session.environment._id,
tags: ['tag2', 'tag3', null, '', undefined],
});

const { body } = await session.testAgent.get(`/v2/environments/${session.environment._id}/tags`);

expect(body.data).to.be.an('array');
expect(body.data).to.have.lengthOf(3);
expect(body.data).to.deep.include({ name: 'tag1' });
expect(body.data).to.deep.include({ name: 'tag2' });
expect(body.data).to.deep.include({ name: 'tag3' });
});

it('should return an empty array when no tags exist', async () => {
const newEnvironment = await environmentRepository.create({
name: 'Test Environment',
_organizationId: session.organization._id,
});

const { body } = await session.testAgent.get(`/v2/environments/${newEnvironment._id}/tags`);

expect(body.data).to.be.an('array');
expect(body.data).to.have.lengthOf(0);
});

it('should throw NotFoundException for non-existent environment', async () => {
const nonExistentId = '60a5f2f2f2f2f2f2f2f2f2f2';
const { body } = await session.testAgent.get(`/v2/environments/${nonExistentId}/tags`);

expect(body.statusCode).to.equal(404);
expect(body.message).to.equal(`Environment ${nonExistentId} not found`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { EnvironmentEntity, EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal';
import { GetEnvironmentTagsCommand } from './get-environment-tags.command';
import { GetEnvironmentTagsDto } from '../../dtos/get-environment-tags.dto';

@Injectable()
export class GetEnvironmentTags {
constructor(
private environmentRepository: EnvironmentRepository,
private notificationTemplateRepository: NotificationTemplateRepository
) {}

async execute(command: GetEnvironmentTagsCommand): Promise<GetEnvironmentTagsDto[]> {
const environment: Omit<EnvironmentEntity, 'apiKeys'> | null = await this.environmentRepository.findOne(
{
_id: command.environmentId,
_organizationId: command.organizationId,
},
'-apiKeys'
);

if (!environment) throw new NotFoundException(`Environment ${command.environmentId} not found`);

const notificationTemplates = await this.notificationTemplateRepository.find({
_environmentId: command.environmentId,
tags: { $exists: true, $type: 'array', $ne: [] },
});

const tags = notificationTemplates.flatMap((template) => template.tags);
const uniqueTags = Array.from(new Set(tags));

return this.sanitizeTags(uniqueTags);
}

private sanitizeTags(tags: string[]): GetEnvironmentTagsDto[] {
return tags.filter((tag) => tag != null && tag !== '').map((tag) => ({ name: tag }));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './get-environment-tags.command';
export * from './get-environment-tags.usecase';
4 changes: 2 additions & 2 deletions apps/api/src/app/organization/organization.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { AuthGuard } from '@nestjs/passport';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { isClerkEnabled } from '@novu/shared';
import { EnvironmentsModule } from '../environments/environments.module';
import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module';
import { IntegrationModule } from '../integrations/integrations.module';
import { SharedModule } from '../shared/shared.module';
import { UserModule } from '../user/user.module';
Expand Down Expand Up @@ -48,7 +48,7 @@ function getControllers() {
imports: [
SharedModule,
UserModule,
EnvironmentsModule,
EnvironmentsModuleV1,
IntegrationModule,
forwardRef(() => AuthModule),
...enterpriseImports(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { ApiServiceLevelEnum, JobTitleEnum, MemberRoleEnum } from '@novu/shared'
import { AnalyticsService } from '@novu/application-generic';

import { ModuleRef } from '@nestjs/core';
import { CreateEnvironmentCommand } from '../../../environments/usecases/create-environment/create-environment.command';
import { CreateEnvironment } from '../../../environments/usecases/create-environment/create-environment.usecase';
import { CreateEnvironmentCommand } from '../../../environments-v1/usecases/create-environment/create-environment.command';
import { CreateEnvironment } from '../../../environments-v1/usecases/create-environment/create-environment.usecase';
import { GetOrganizationCommand } from '../get-organization/get-organization.command';
import { GetOrganization } from '../get-organization/get-organization.usecase';
import { AddMemberCommand } from '../membership/add-member/add-member.command';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { OrganizationEntity, OrganizationRepository, UserRepository } from '@nov
import { AnalyticsService } from '@novu/application-generic';

import { ModuleRef } from '@nestjs/core';
import { CreateEnvironmentCommand } from '../../../../environments/usecases/create-environment/create-environment.command';
import { CreateEnvironment } from '../../../../environments/usecases/create-environment/create-environment.usecase';
import { CreateEnvironmentCommand } from '../../../../environments-v1/usecases/create-environment/create-environment.command';
import { CreateEnvironment } from '../../../../environments-v1/usecases/create-environment/create-environment.usecase';
import { GetOrganizationCommand } from '../../get-organization/get-organization.command';
import { GetOrganization } from '../../get-organization/get-organization.usecase';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { CommunityUserRepository, CommunityOrganizationRepository } from '@novu/
import { USE_CASES } from './usecases';
import { PartnerIntegrationsController } from './partner-integrations.controller';
import { SharedModule } from '../shared/shared.module';
import { EnvironmentsModule } from '../environments/environments.module';
import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module';
import { BridgeModule } from '../bridge';

@Module({
imports: [SharedModule, HttpModule, EnvironmentsModule, BridgeModule],
imports: [SharedModule, HttpModule, EnvironmentsModuleV1, BridgeModule],
providers: [...USE_CASES, CommunityUserRepository, CommunityOrganizationRepository],
controllers: [PartnerIntegrationsController],
})
Expand Down

0 comments on commit 9f2c5a3

Please sign in to comment.