Skip to content

Commit

Permalink
feat(api): create topic use case and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
p-fernandez committed Nov 30, 2022
1 parent 32a79ed commit 3be1c62
Show file tree
Hide file tree
Showing 18 changed files with 226 additions and 16 deletions.
3 changes: 3 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RavenInterceptor, RavenModule } from 'nest-raven';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';

import { SharedModule } from './app/shared/shared.module';
import { UserModule } from './app/user/user.module';
import { AuthModule } from './app/auth/auth.module';
Expand All @@ -27,6 +28,7 @@ import { SubscribersModule } from './app/subscribers/subscribers.module';
import { FeedsModule } from './app/feeds/feeds.module';
import { MessagesModule } from './app/messages/messages.module';
import { PartnerIntegrationsModule } from './app/partner-integrations/partner-integrations.module';
import { TopicsModule } from './app/topics/topics.module';

const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [
OrganizationModule,
Expand All @@ -51,6 +53,7 @@ const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardRefe
FeedsModule,
MessagesModule,
PartnerIntegrationsModule,
TopicsModule,
];

const providers = [];
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
JobRepository,
FeedRepository,
SubscriberPreferenceRepository,
TopicRepository,
} from '@novu/dal';
import { AnalyticsService } from './services/analytics/analytics.service';
import { QueueService } from './services/queue';
Expand Down Expand Up @@ -46,6 +47,7 @@ const DAL_MODELS = [
JobRepository,
FeedRepository,
SubscriberPreferenceRepository,
TopicRepository,
];

function getStorageServiceClass() {
Expand Down
23 changes: 23 additions & 0 deletions apps/api/src/app/topics/dtos/create-topic.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDefined, IsString } from 'class-validator';

import { TopicDto } from './topic.dto';

export class CreateTopicResponseDto implements Pick<TopicDto, '_id'> {}

export class CreateTopicRequestDto {
@ApiProperty({
description:
'User defined custom key and provided by the user that will be an unique identifier for the Topic created.',
})
@IsString()
@IsDefined()
key: string;

@ApiProperty({
description: 'User defined custom name and provided by the user that will name the Topic created.',
})
@IsString()
@IsDefined()
name: string;
}
21 changes: 21 additions & 0 deletions apps/api/src/app/topics/dtos/topic.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class TopicDto {
@ApiProperty()
_id?: string;

@ApiProperty()
_organizationId: string;

@ApiProperty()
_environmentId: string;

@ApiProperty()
_userId: string;

@ApiProperty()
key: string;

@ApiProperty()
name: string;
}
44 changes: 44 additions & 0 deletions apps/api/src/app/topics/e2e/create-topic.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { UserSession } from '@novu/testing';
import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { IJwtPayload, MemberRoleEnum } from '@novu/shared';

const URL = '/v1/topics';

describe('Topic creation - /topics (POST)', async () => {
let session: UserSession;

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

it('should throw validation error for missing request payload information', async () => {
const { body } = await session.testAgent.post(URL).send({});

expect(body.statusCode).to.equal(400);
expect(body.message.find((i) => i.includes('key'))).to.be.ok;
expect(body.message.find((i) => i.includes('name'))).to.be.ok;
expect(body.message).to.eql([
'key should not be null or undefined',
'key must be a string',
'name should not be null or undefined',
'name must be a string',
]);
});

it('should create a new topic successfully', async () => {
const topicKey = 'topic-key';
const topicName = 'topic-name';
const response = await session.testAgent.post(URL).send({
key: topicKey,
name: topicName,
});

expect(response.statusCode).to.eql(201);

const { body } = response;
expect(body.data._id).to.exist;
expect(body.data._id).to.be.string;
});
});
46 changes: 46 additions & 0 deletions apps/api/src/app/topics/topics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
import { ApiExcludeController, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { IJwtPayload } from '@novu/shared';

import { CreateTopicRequestDto, CreateTopicResponseDto } from './dtos/create-topic.dto';
import { CreateTopicCommand, CreateTopicUseCase } from './use-cases';

import { JwtAuthGuard } from '../auth/framework/auth.guard';
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
import { UserSession } from '../shared/framework/user.decorator';
import { AnalyticsService } from '../shared/services/analytics/analytics.service';
import { ANALYTICS_SERVICE } from '../shared/shared.module';

@Controller('/topics')
@ApiTags('Topics')
@UseGuards(JwtAuthGuard)
export class TopicsController {
constructor(
private createTopicUseCase: CreateTopicUseCase,
@Inject(ANALYTICS_SERVICE) private analyticsService: AnalyticsService
) {}

@ExternalApiAccessible()
@ApiOkResponse({
type: CreateTopicResponseDto,
})
@Post('/')
async createTopic(
@UserSession() user: IJwtPayload,
@Body() body: CreateTopicRequestDto
): Promise<CreateTopicResponseDto> {
const topic = await this.createTopicUseCase.execute(
CreateTopicCommand.create({
environmentId: user.environmentId,
key: body.key,
name: body.name,
organizationId: user.organizationId,
userId: user._id,
})
);

return {
_id: topic._id,
};
}
}
15 changes: 15 additions & 0 deletions apps/api/src/app/topics/topics.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';

import { USE_CASES } from './use-cases';
import { TopicsController } from './topics.controller';

import { SharedModule } from '../shared/shared.module';
import { AuthModule } from '../auth/auth.module';

@Module({
imports: [SharedModule, AuthModule],
providers: [...USE_CASES],
exports: [...USE_CASES],
controllers: [TopicsController],
})
export class TopicsModule {}
1 change: 1 addition & 0 deletions apps/api/src/app/topics/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EnvironmentId, OrganizationId, TopicId, TopicKey, UserId } from '@novu/dal';
14 changes: 14 additions & 0 deletions apps/api/src/app/topics/use-cases/create-topic.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TopicKey } from '@novu/dal';
import { IsDefined, IsString } from 'class-validator';

import { EnvironmentWithUserCommand } from '../../shared/commands/project.command';

export class CreateTopicCommand extends EnvironmentWithUserCommand {
@IsString()
@IsDefined()
key: TopicKey;

@IsString()
@IsDefined()
name: string;
}
33 changes: 33 additions & 0 deletions apps/api/src/app/topics/use-cases/create-topic.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { EnvironmentId, OrganizationId, TopicEntity, TopicKey, TopicRepository, UserId } from '@novu/dal';

import { CreateTopicCommand } from './create-topic.command';

import { TopicDto } from '../dtos/topic.dto';

const mapFromCommandToRepository = (command: CreateTopicCommand) => ({
_environmentId: command.environmentId as unknown as EnvironmentId,
_organizationId: command.organizationId as unknown as OrganizationId,
_userId: command.userId as unknown as UserId,
key: command.key as TopicKey,
name: command.name,
});

const mapFromEntityToDto = (topic: TopicEntity): TopicDto => ({
...topic,
_id: topic._id as unknown as string,
_organizationId: topic._organizationId as unknown as string,
_environmentId: topic._environmentId as unknown as string,
_userId: topic._userId as unknown as string,
});

@Injectable()
export class CreateTopicUseCase {
constructor(private topicRepository: TopicRepository) {}

async execute(command: CreateTopicCommand) {
const topic = await this.topicRepository.create(mapFromCommandToRepository(command));

return mapFromEntityToDto(topic);
}
}
6 changes: 6 additions & 0 deletions apps/api/src/app/topics/use-cases/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CreateTopicUseCase } from './create-topic.use-case';

export * from './create-topic.command';
export * from './create-topic.use-case';

export const USE_CASES = [CreateTopicUseCase];
1 change: 1 addition & 0 deletions libs/dal/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export * from './repositories/job';
export * from './repositories/feed';
export * from './repositories/execution-details';
export * from './repositories/subscriber-preference';
export * from './repositories/topic';
export * from './shared/exceptions';
1 change: 1 addition & 0 deletions libs/dal/src/repositories/topic/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './types';
export * from './topic.entity';
export * from './topic.repository';
export * from './topic-subscribers.entity';
Expand Down
4 changes: 1 addition & 3 deletions libs/dal/src/repositories/topic/topic-subscribers.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Types } from 'mongoose';

import { EnvironmentId, OrganizationId, TopicId, UserId } from './topic.entity';
import { EnvironmentId, OrganizationId, TopicId, UserId } from './types';

import { SubscriberId } from '../subscriber/subscriber.entity';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AuthProviderEnum } from '@novu/shared';
import { FilterQuery } from 'mongoose';

import { EnvironmentId, OrganizationId } from './topic.entity';
import { TopicSubscribersEntity } from './topic-subscribers.entity';
import { TopicSubscribers } from './topic-subscribers.schema';
import { EnvironmentId, OrganizationId } from './types';

import { BaseRepository } from '../base-repository';

Expand Down
10 changes: 2 additions & 8 deletions libs/dal/src/repositories/topic/topic.entity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { Types } from 'mongoose';

export type EnvironmentId = Types.ObjectId;
export type OrganizationId = Types.ObjectId;
export type TopicId = Types.ObjectId;
export type TopicKey = string;
export type UserId = Types.ObjectId;
import { EnvironmentId, OrganizationId, TopicId, TopicKey, UserId } from './types';

export class TopicEntity {
_id: TopicId;
Expand All @@ -14,5 +8,5 @@ export class TopicEntity {
key: TopicKey;
name: string;
// For users to have related anything they want with the topic.
customData: Record<string, unknown>;
customData?: Record<string, unknown>;
}
9 changes: 5 additions & 4 deletions libs/dal/src/repositories/topic/topic.repository.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AuthProviderEnum } from '@novu/shared';
import { FilterQuery } from 'mongoose';

import { EnvironmentId, OrganizationId, TopicKey, TopicEntity } from './topic.entity';
import { TopicEntity } from './topic.entity';
import { Topic } from './topic.schema';
import { EnvironmentId, OrganizationId, TopicKey } from './types';

import { BaseRepository } from '../base-repository';
import { BaseRepository, Omit } from '../base-repository';

type IPartialTopicEntity = Omit<TopicEntity, '_environmentId' | '_organizationId'>;
class PartialIntegrationEntity extends Omit(TopicEntity, ['_environmentId', '_organizationId']) {}

type EnforceEnvironmentQuery = FilterQuery<IPartialTopicEntity> &
type EnforceEnvironmentQuery = FilterQuery<PartialIntegrationEntity> &
({ _environmentId: EnvironmentId } | { _organizationId: OrganizationId });

export class TopicRepository extends BaseRepository<EnforceEnvironmentQuery, TopicEntity> {
Expand Down
7 changes: 7 additions & 0 deletions libs/dal/src/repositories/topic/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Schema } from 'mongoose';

export type EnvironmentId = Schema.Types.ObjectId;
export type OrganizationId = Schema.Types.ObjectId;
export type TopicId = Schema.Types.ObjectId;
export type TopicKey = string;
export type UserId = Schema.Types.ObjectId;

0 comments on commit 3be1c62

Please sign in to comment.