Skip to content

Commit

Permalink
docs(api): Documentation Changes - updating and removing endpoints fr…
Browse files Browse the repository at this point in the history
…om swagger (#5687)

* refactor: readability changes

refactor: readability changes

refactor: fix annotations

refactor: renaming

refactor: built after rebase

refactor: added hoisted endpoint

refactor: security clean up

* refactor: readability changes

* refactor: readability changes

* refactor: readability changes

* refactor: readability changes

* refactor: spelling

* refactor: fix imports

* refactor: fix imports

* refactor: fix imports

* refactor: move trigger
  • Loading branch information
tatarco authored Jun 14, 2024
1 parent 93aa727 commit c72cd3c
Show file tree
Hide file tree
Showing 75 changed files with 930 additions and 606 deletions.
6 changes: 5 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,11 @@
"zwnj",
"prepush",
"xcodebuild",
"liquidjs"
"liquidjs",
"nimma",
"jpath",
"Nimma",
"jpath"
],
"flagWords": [],
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion .idea/nx-angular-config.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .source
45 changes: 32 additions & 13 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,6 @@

A RESTful API for accessing the Novu platform, built using [NestJS](https://nestjs.com/).

## OpenAPI (formerly Swagger)

The Novu API utilizes the [`@nestjs/swagger`](https://github.com/nestjs/swagger) package to generate up-to-date OpenAPI specifications.

A web interface to browse the published endpoints is available during local development at [localhost:3000/openapi](https://localhost:3000/openapi). An OpenAPI specification can be retrieved at [api.novu.co/openapi.yaml](https://api.novu.co/openapi.yaml).

To maintain consistency and quality of OpenAPI documentation, Novu uses [Spectral](https://github.com/stoplightio/spectral) to validate the OpenAPI specification and enforce style. The OpenAPI specification is run through a Github action on pull request, and call also be run locally after starting the API with:

```bash
$ npm run lint:openapi
```

The command will return warnings and errors that must be fixed before the Github action will pass. These fixes are created by making changes through the `@nestjs/swagger` decorators.

## Running the API

Expand All @@ -45,6 +32,38 @@ $ npm run test
### E2E tests
See the docs for [Running on Local Machine - API Tests](https://docs.novu.co/community/run-in-local-machine#api?utm_campaign=github-api-readme).

## Adding a new Endpoint
### Choose the right controller / new controller.
- If the endpoint is related to an existing entity, add the endpoint to the existing controller.
### Add the correct decorators to the controller method.
- Use the `@Get`, `@Post`, `@Put`, `@Delete` decorators to define the HTTP method.
- Use the `@Param`, `@Query`, `@Body` decorators to define the parameters.
- Use the `@UserAuthentication()` decorator to define the guards as well as make it accessible to novu web app.
- Use the @ExternalApiAccessible decorator to define the endpoint as accessible by external API (Users with Api-Key) & The official Novu SDK.
#### Naming conventions
- for the controller methods should be in the format `getEntityName`, `createEntityName`, `updateEntityName`, `deleteEntityName`.
- In Case of a getAll / List use the `list` prefix for the method name and don't forget to add pagination functionality.
- Use the `@SdkUsePagination` decorator to alert the sdk of a paginated endpoint (will improve DX with an async iterator) the pagination parameters.
- In case of a uniuqe usecase outside of the basic REST operations, attempt to use the regular naming conventions just for a sub-resource.
- `@SdkGroupName` - Use this decorator to group the endpoints in the SDK, use `.` separator to create a subresource (Ex' 'Subscribers.Notifications' getSubscriberNotifications), the original resource is defined as an openApi Tag .
- `@SdkMethodName` in case of a unique operation, use this decorator to define the method name in the SDK.


## OpenAPI (formerly Swagger)

The Novu API utilizes the [`@nestjs/swagger`](https://github.com/nestjs/swagger) package to generate up-to-date OpenAPI specifications.

A web interface to browse the published endpoints is available during local development at [localhost:3000/openapi](https://localhost:3000/openapi). An OpenAPI specification can be retrieved at [api.novu.co/openapi.yaml](https://api.novu.co/openapi.yaml).

To maintain consistency and quality of OpenAPI documentation, Novu uses [Spectral](https://github.com/stoplightio/spectral) to validate the OpenAPI specification and enforce style. The OpenAPI specification is run through a Github action on pull request, and call also be run locally after starting the API with:

```bash
$ npm run lint:openapi
```

The command will return warnings and errors that must be fixed before the Github action will pass. These fixes are created by making changes through the `@nestjs/swagger` decorators.


## Migrations
Database migrations are included for features that have a hard dependency on specific data being available on database entities. These migrations are run by both Novu Cloud and Novu Self-Hosted users to support new feature releases.

Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"bcrypt": "^5.0.0",
"body-parser": "^1.20.0",
"bull": "^4.2.1",
"nimma": "^0.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4",
Expand Down
13 changes: 8 additions & 5 deletions apps/api/src/app/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Body, Controller, Logger, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Post } from '@nestjs/common';
import { SkipThrottle } from '@nestjs/throttler';
import { AnalyticsService, UserAuthGuard, UserSession } from '@novu/application-generic';
import { IJwtPayload } from '@novu/shared';
import { AnalyticsService, UserSession } from '@novu/application-generic';
import { UserSessionData } from '@novu/shared';
import { ApiExcludeController } from '@nestjs/swagger';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';

@Controller({
path: 'telemetry',
})
@SkipThrottle()
@ApiExcludeController()
export class AnalyticsController {
constructor(private analyticsService: AnalyticsService) {}

@Post('/measure')
@UseGuards(UserAuthGuard)
async trackEvent(@Body('event') event, @Body('data') data = {}, @UserSession() user: IJwtPayload): Promise<any> {
@UserAuthentication()
async trackEvent(@Body('event') event, @Body('data') data = {}, @UserSession() user: UserSessionData): Promise<any> {
this.analyticsService.track(event, user._id, {
...(data || {}),
_organization: user?.organizationId,
Expand Down
28 changes: 14 additions & 14 deletions apps/api/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
ClassSerializerInterceptor,
Controller,
Get,
Header,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Param,
Post,
Expand All @@ -13,21 +16,17 @@ import {
Res,
UseGuards,
UseInterceptors,
Logger,
Header,
HttpStatus,
} from '@nestjs/common';
import { MemberRepository, OrganizationRepository, UserRepository, MemberEntity } from '@novu/dal';
import { MemberEntity, MemberRepository, OrganizationRepository, UserRepository } from '@novu/dal';
import { AuthGuard } from '@nestjs/passport';
import { IJwtPayload, PasswordResetFlowEnum } from '@novu/shared';
import { PasswordResetFlowEnum, UserSessionData } from '@novu/shared';
import { UserRegistrationBodyDto } from './dtos/user-registration.dto';
import { UserRegister } from './usecases/register/user-register.usecase';
import { UserRegisterCommand } from './usecases/register/user-register.command';
import { Login } from './usecases/login/login.usecase';
import { LoginBodyDto } from './dtos/login.dto';
import { LoginCommand } from './usecases/login/login.command';
import { UserSession } from '../shared/framework/user.decorator';
import { UserAuthGuard } from './framework/user.auth.guard';
import { PasswordResetRequestCommand } from './usecases/password-reset-request/password-reset-request.command';
import { PasswordResetRequest } from './usecases/password-reset-request/password-reset-request.usecase';
import { PasswordResetCommand } from './usecases/password-reset/password-reset.command';
Expand All @@ -47,6 +46,7 @@ import { ApiCommonResponses } from '../shared/framework/response.decorator';
import { UpdatePasswordBodyDto } from './dtos/update-password.dto';
import { UpdatePassword } from './usecases/update-password/update-password.usecase';
import { UpdatePasswordCommand } from './usecases/update-password/update-password.command';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';

@ApiCommonResponses()
@Controller('/auth')
Expand Down Expand Up @@ -94,9 +94,9 @@ export class AuthController {
}

@Get('/refresh')
@UseGuards(UserAuthGuard)
@UserAuthentication()
@Header('Cache-Control', 'no-store')
refreshToken(@UserSession() user: IJwtPayload) {
refreshToken(@UserSession() user: UserSessionData) {
if (!user || !user._id) throw new BadRequestException();

return this.authService.refreshToken(user._id);
Expand Down Expand Up @@ -152,11 +152,11 @@ export class AuthController {
}

@Post('/organizations/:organizationId/switch')
@UseGuards(UserAuthGuard)
@UserAuthentication()
@HttpCode(200)
@Header('Cache-Control', 'no-store')
async organizationSwitch(
@UserSession() user: IJwtPayload,
@UserSession() user: UserSessionData,
@Param('organizationId') organizationId: string
): Promise<string> {
const command = SwitchOrganizationCommand.create({
Expand All @@ -169,10 +169,10 @@ export class AuthController {

@Post('/environments/:environmentId/switch')
@Header('Cache-Control', 'no-store')
@UseGuards(UserAuthGuard)
@UserAuthentication()
@HttpCode(200)
async projectSwitch(
@UserSession() user: IJwtPayload,
@UserSession() user: UserSessionData,
@Param('environmentId') environmentId: string
): Promise<{ token: string }> {
const command = SwitchEnvironmentCommand.create({
Expand All @@ -188,9 +188,9 @@ export class AuthController {

@Post('/update-password')
@Header('Cache-Control', 'no-store')
@UseGuards(UserAuthGuard)
@UserAuthentication()
@HttpCode(HttpStatus.NO_CONTENT)
async updatePassword(@UserSession() user: IJwtPayload, @Body() body: UpdatePasswordBodyDto) {
async updatePassword(@UserSession() user: UserSessionData, @Body() body: UpdatePasswordBodyDto) {
return await this.updatePasswordUsecase.execute(
UpdatePasswordCommand.create({
userId: user._id,
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/app/auth/e2e/login.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UserSession } from '@novu/testing';
import * as jwt from 'jsonwebtoken';
import { subMinutes } from 'date-fns';
import { expect } from 'chai';
import { IJwtPayload } from '@novu/shared';
import { UserSessionData } from '@novu/shared';
import { UserRepository } from '@novu/dal';

describe('User login - /auth/login (POST)', async () => {
Expand Down Expand Up @@ -35,7 +35,7 @@ describe('User login - /auth/login (POST)', async () => {
password: userCredentials.password,
});

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal('test');
expect(jwtContent.lastName).to.equal('user');
Expand All @@ -48,7 +48,7 @@ describe('User login - /auth/login (POST)', async () => {
password: userCredentials.password,
});

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal('test');
expect(jwtContent.lastName).to.equal('user');
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('User login - /auth/login (POST)', async () => {
password: userCredentials.password,
});

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal('test');
expect(jwtContent.lastName).to.equal('user');
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/app/auth/e2e/switch-environment.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { EnvironmentEntity } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { IJwtPayload } from '@novu/shared';
import { UserSessionData } from '@novu/shared';

describe('Switch Environment - /auth/environments/:id/switch (POST)', async () => {
let session: UserSession;
Expand All @@ -19,15 +19,15 @@ describe('Switch Environment - /auth/environments/:id/switch (POST)', async () =
});

it('should switch to second environment', async () => {
const content = jwt.decode(session.token.split(' ')[1]) as IJwtPayload;
const content = jwt.decode(session.token.split(' ')[1]) as UserSessionData;

expect(content.environmentId).to.equal(firstEnvironment._id);

const { body } = await session.testAgent
.post(`/v1/auth/environments/${secondEnvironment._id}/switch`)
.expect(200);

const newJwt = jwt.decode(body.data.token) as IJwtPayload;
const newJwt = jwt.decode(body.data.token) as UserSessionData;

expect(newJwt._id).to.equal(session.user._id);
expect(newJwt.organizationId).to.equal(session.organization._id);
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/app/auth/e2e/switch-organization.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { OrganizationEntity } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { IJwtPayload, MemberRoleEnum } from '@novu/shared';
import { MemberRoleEnum, UserSessionData } from '@novu/shared';

describe('Switch Organization - /auth/organizations/:id/switch (POST)', async () => {
let session: UserSession;
Expand Down Expand Up @@ -31,14 +31,14 @@ describe('Switch Organization - /auth/organizations/:id/switch (POST)', async ()
});

it('should switch the user current organization', async () => {
const content = jwt.decode(session.token.split(' ')[1]) as IJwtPayload;
const content = jwt.decode(session.token.split(' ')[1]) as UserSessionData;

expect(content._id).to.equal(session.user._id);
const organization = await session.addOrganization();

const { body } = await session.testAgent.post(`/v1/auth/organizations/${organization._id}/switch`).expect(200);

const newJwt = jwt.decode(body.data) as IJwtPayload;
const newJwt = jwt.decode(body.data) as UserSessionData;

expect(newJwt._id).to.equal(session.user._id);
expect(newJwt.organizationId).to.equal(organization._id);
Expand All @@ -59,15 +59,15 @@ describe('Switch Organization - /auth/organizations/:id/switch (POST)', async ()
});

it('should switch to second organization', async () => {
const content = jwt.decode(session.token.split(' ')[1]) as IJwtPayload;
const content = jwt.decode(session.token.split(' ')[1]) as UserSessionData;

expect(content.organizationId).to.equal(firstOrganization._id);

const { body } = await session.testAgent
.post(`/v1/auth/organizations/${secondOrganization._id}/switch`)
.expect(200);

const newJwt = jwt.decode(body.data) as IJwtPayload;
const newJwt = jwt.decode(body.data) as UserSessionData;

expect(newJwt._id).to.equal(session.user._id);
expect(newJwt.organizationId).to.equal(secondOrganization._id);
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/e2e/update-password.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IJwtPayload } from '@novu/shared';
import { UserSessionData } from '@novu/shared';
import { TEST_USER_PASSWORD, UserSession } from '@novu/testing';
import { expect } from 'chai';
import * as jwt from 'jsonwebtoken';
Expand Down Expand Up @@ -30,7 +30,7 @@ describe('User update password - /auth/update-password (POST)', async () => {
password: NEW_PASSWORD,
});

const jwtContent = (await jwt.decode(loginBody.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(loginBody.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal(session.user.firstName);
expect(jwtContent.lastName).to.equal(session.user.lastName);
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/app/auth/e2e/user-registration.e2e-ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EnvironmentRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { IJwtPayload } from '@novu/shared';
import { UserSessionData } from '@novu/shared';

describe('User registration in enterprise - /auth/register (POST)', async () => {
let session: UserSession;
Expand All @@ -24,11 +24,11 @@ describe('User registration in enterprise - /auth/register (POST)', async () =>

expect(body.data.token).to.be.ok;

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.environmentId).to.be.ok;
const environment = await environmentRepository.findOne({ _id: jwtContent.environmentId });

expect(environment.echo.url).to.be.ok;
expect(environment?.echo.url).to.be.ok;
});
});
6 changes: 3 additions & 3 deletions apps/api/src/app/auth/e2e/user-registration.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EnvironmentRepository, OrganizationRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import * as jwt from 'jsonwebtoken';
import { expect } from 'chai';
import { IJwtPayload, MemberRoleEnum } from '@novu/shared';
import { MemberRoleEnum, UserSessionData } from '@novu/shared';

describe('User registration - /auth/register (POST)', async () => {
let session: UserSession;
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('User registration - /auth/register (POST)', async () => {

expect(body.data.token).to.be.ok;

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal('test');
expect(jwtContent.lastName).to.equal('user');
Expand All @@ -69,7 +69,7 @@ describe('User registration - /auth/register (POST)', async () => {

expect(body.data.token).to.be.ok;

const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload;
const jwtContent = (await jwt.decode(body.data.token)) as UserSessionData;

expect(jwtContent.firstName).to.equal('test');
expect(jwtContent.lastName).to.equal('user');
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/app/auth/framework/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IJwtPayload } from '@novu/shared';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class RolesGuard implements CanActivate {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CanActivate, ExecutionContext, forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IJwtPayload } from '@novu/shared';
import * as jwt from 'jsonwebtoken';
import { AuthService } from '@novu/application-generic';

@Injectable()
Expand Down
Loading

0 comments on commit c72cd3c

Please sign in to comment.