Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dal, shared, api): Add rate limit DAL attributes #4758

Merged
merged 8 commits into from
Nov 6, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,14 @@ describe('Create Environment - /environments (POST)', async () => {
expect(layouts.data[0].isDefault).to.equal(true);
expect(layouts.data[0].content.length).to.be.greaterThan(20);
});

it('should not set apiRateLimits field on environment by default', async function () {
const demoEnvironment = {
name: 'Hello App',
};
const { body } = await session.testAgent.post('/v1/environments').send(demoEnvironment).expect(201);
const dbEnvironment = await environmentRepository.findOne({ _id: body.data._id });

expect(dbEnvironment?.apiRateLimits).to.be.undefined;
});
});
13 changes: 12 additions & 1 deletion apps/api/src/app/organization/e2e/create-organization.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MemberRepository, OrganizationRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { MemberRoleEnum } from '@novu/shared';
import { ApiServiceLevelTypeEnum, MemberRoleEnum } from '@novu/shared';
import { expect } from 'chai';

describe('Create Organization - /organizations (POST)', async () => {
Expand Down Expand Up @@ -44,5 +44,16 @@ describe('Create Organization - /organizations (POST)', async () => {
it('should not create organization with no name', async () => {
await session.testAgent.post('/v1/organizations').send({}).expect(400);
});

it('should create organization with apiServiceLevel of free by default', async () => {
const testOrganization = {
name: 'Free Org',
};

const { body } = await session.testAgent.post('/v1/organizations').send(testOrganization).expect(201);
const dbOrganization = await organizationRepository.findById(body.data._id);

expect(dbOrganization?.apiServiceLevel).to.eq(ApiServiceLevelTypeEnum.FREE);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal';
import { MemberRoleEnum } from '@novu/shared';
import { ApiServiceLevelTypeEnum, MemberRoleEnum } from '@novu/shared';
import { AnalyticsService } from '@novu/application-generic';

import { CreateEnvironmentCommand } from '../../../environments/usecases/create-environment/create-environment.command';
Expand Down Expand Up @@ -30,15 +30,14 @@ export class CreateOrganization {
) {}

async execute(command: CreateOrganizationCommand): Promise<OrganizationEntity> {
const organization = new OrganizationEntity();

organization.logo = command.logo;
organization.name = command.name;

const user = await this.userRepository.findById(command.userId);
if (!user) throw new ApiException('User not found');

const createdOrganization = await this.organizationRepository.create(organization);
const createdOrganization = await this.organizationRepository.create({
logo: command.logo,
name: command.name,
apiServiceLevel: ApiServiceLevelTypeEnum.FREE,
});

await this.addMemberUsecase.execute(
AddMemberCommand.create({
Expand Down
3 changes: 3 additions & 0 deletions libs/dal/src/repositories/environment/environment.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Types } from 'mongoose';

import type { OrganizationId } from '../organization';
import type { ChangePropsValueType } from '../../types/helpers';
import { IApiRateLimits } from '@novu/shared';

export interface IApiKey {
key: string;
Expand All @@ -28,6 +29,8 @@ export class EnvironmentEntity {

apiKeys: IApiKey[];

apiRateLimits: IApiRateLimits;

widget: IWidgetSettings;

dns?: IDnsSettings;
Expand Down
6 changes: 6 additions & 0 deletions libs/dal/src/repositories/environment/environment.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as mongoose from 'mongoose';
import { Schema } from 'mongoose';
import { IApiRateLimits, ApiRateLimitCategoryTypeEnum } from '@novu/shared';

import { schemaOptions } from '../schema-default.options';
import { EnvironmentDBModel } from './environment.entity';
Expand Down Expand Up @@ -28,6 +29,11 @@ const environmentSchema = new Schema<EnvironmentDBModel>(
},
},
],
apiRateLimits: {
[ApiRateLimitCategoryTypeEnum.TRIGGER]: Schema.Types.Number,
[ApiRateLimitCategoryTypeEnum.CONFIGURATION]: Schema.Types.Number,
[ApiRateLimitCategoryTypeEnum.GLOBAL]: Schema.Types.Number,
},
widget: {
notificationCenterEncryption: {
type: Schema.Types.Boolean,
Expand Down
4 changes: 4 additions & 0 deletions libs/dal/src/repositories/organization/organization.entity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { ApiServiceLevelTypeEnum } from '@novu/shared';

export class OrganizationEntity {
_id: string;

name: string;

logo?: string;

apiServiceLevel?: ApiServiceLevelTypeEnum;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rifont should we exclude this entity from returning using the Exclude decorator here? So it won't be returned by default when querying for his org? To me it's more of an internal property. Wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that apiServiceLevel should be excluded from the API response for /organizations due to it being an internal property. Though, I'm not convinced it should be excluded at the database layer - this creates extra work in the use-case layer, complicates typings, and makes debugging for missing properties less transparent and difficult.

I experimented with the ApiHideProperty decorator from @nestjs/swagger, however, this only removes the attribute from the swagger doc.

Longer term, we probably want a way to exclude certain properties from an API response, I see 2 solutions here:

  • Implicitly exclude non-decorated properties by design. Properties intended to be exposed must be decorated on a response DTO. This is a more failsafe option.
  • Explicitly exclude decorated properties. This is more intentional but requires discipline to ensure properties are decorated correctly.

I'll go with the DTO layer exclude approach in absence of the API layer exclusion for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scopsy just thinking on this some more. On the other hand, it could be useful to customers building platforms on top of Novu to have access to the apiServiceLevel, and the apiRateLimits property, to display these in Admin dashboards to aid in debugging/supporting the service they are providing to their own customers via their platform.

This B2B2B2C mindset for Novu as a notification infrastructure will provide the best flexibility and transparency for customers to pick and choose how they provide access to the underlying Novu platform resources with features such as rate limiting.

Copy link
Collaborator Author

@rifont rifont Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the Swagger doc should be seen as the source of truth for API integrations, we can technically merge and proceed with this implementation and modify the behavior of this property prior to release without being a breaking change.

I'll merge this to keep proceedings with the rate-limiting implementation, but let's discuss this further.


branding: {
fontFamily?: string;
fontColor?: string;
Expand Down
5 changes: 5 additions & 0 deletions libs/dal/src/repositories/organization/organization.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as mongoose from 'mongoose';
import { Schema } from 'mongoose';
import { ApiServiceLevelTypeEnum } from '@novu/shared';

import { schemaOptions } from '../schema-default.options';
import { OrganizationDBModel, PartnerTypeEnum } from './organization.entity';
Expand All @@ -8,6 +9,10 @@ const organizationSchema = new Schema<OrganizationDBModel>(
{
name: Schema.Types.String,
logo: Schema.Types.String,
apiServiceLevel: {
type: Schema.Types.String,
enum: ApiServiceLevelTypeEnum,
},
branding: {
fontColor: Schema.Types.String,
contentBackground: Schema.Types.String,
Expand Down
3 changes: 3 additions & 0 deletions libs/shared/src/entities/environment/environment.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IApiRateLimits } from '../../types';

export interface IEnvironment {
_id?: string;
name: string;
Expand All @@ -6,6 +8,7 @@ export interface IEnvironment {
identifier: string;
widget: IWidgetSettings;
dns?: IDnsSettings;
apiRateLimits?: IApiRateLimits;

branding?: {
color: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ApiServiceLevelTypeEnum } from '../../types';
import { IUserEntity } from '../user';
import { MemberRoleEnum } from './member.enum';
import { IMemberInvite, MemberStatusEnum } from './member.interface';

export interface IOrganizationEntity {
_id: string;
name: string;
apiServiceLevel?: ApiServiceLevelTypeEnum;
members: {
_id: string;
_userId?: string;
Expand Down
8 changes: 8 additions & 0 deletions libs/shared/src/types/environment/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export type EnvironmentId = string;

export enum ApiRateLimitCategoryTypeEnum {
TRIGGER = 'trigger',
CONFIGURATION = 'configuration',
GLOBAL = 'global',
}

export type IApiRateLimits = Record<ApiRateLimitCategoryTypeEnum, number>;
5 changes: 5 additions & 0 deletions libs/shared/src/types/organization/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export type OrganizationId = string;

export enum ApiServiceLevelTypeEnum {
FREE = 'free',
BUSINESS = 'business',
}
Loading