Skip to content

Commit

Permalink
feat: Adding Swagger documentation to the NestJS-mod application and …
Browse files Browse the repository at this point in the history
…generating a REST client for the Angular application (2024-08-14)
  • Loading branch information
EndyKaufman committed Aug 15, 2024
1 parent cb61603 commit 0353b23
Show file tree
Hide file tree
Showing 74 changed files with 7,542 additions and 1,012 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
libs/sdk/app-rest-sdk/src/lib
libs/sdk/app-angular-rest-sdk/src/lib
28 changes: 28 additions & 0 deletions .verdaccio/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# path to a directory with all packages
storage: ../tmp/local-registry/storage

# a list of other known repositories we can talk to
uplinks:
npmjs:
url: https://registry.npmjs.org/
maxage: 60m

packages:
'**':
# give all users (including non-authenticated users) full access
# because it is a local registry
access: $all
publish: $all
unpublish: $all

# if package is not available locally, proxy requests to npm registry
proxy: npmjs

# log settings
logs:
type: stdout
format: pretty
level: warn

publish:
allow_offline: true # set offline to true to allow publish offline
1 change: 1 addition & 0 deletions app-swagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"openapi":"3.0.0","paths":{"/api/health":{"get":{"operationId":"TerminusHealthCheckController_check","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","example":{},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","example":{"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"},"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}}}}},"/api":{"get":{"operationId":"AppController_getData","parameters":[],"responses":{"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppData"}}}}}}},"/api/demo":{"post":{"operationId":"AppController_demoCreateOne","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDemo"}}}}}},"get":{"operationId":"AppController_demoFindMany","parameters":[],"responses":{"default":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppDemo"}}}}}}}},"/api/demo/{id}":{"get":{"operationId":"AppController_demoFindOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDemo"}}}}}},"delete":{"operationId":"AppController_demoDeleteOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDemo"}}}}}}}},"info":{"title":"","description":"","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"AppData":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"AppDemo":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","createdAt","updatedAt"]}}}}
10 changes: 8 additions & 2 deletions apps/client/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { HttpClientModule } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { RestClientApiModule, RestClientConfiguration } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
import { RouterModule } from '@angular/router';

describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent, NxWelcomeComponent, RouterModule.forRoot([])],
imports: [AppComponent, NxWelcomeComponent, RouterModule.forRoot([]), HttpClientModule, RestClientApiModule.forRoot(() => new RestClientConfiguration(
{
basePath: 'http://localhost:3000'
}
))],
}).compileComponents();
});

Expand Down
7 changes: 3 additions & 4 deletions apps/client/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { RouterModule } from '@angular/router';
import { DefaultRestService } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { NxWelcomeComponent } from './nx-welcome.component';
import { HttpClient } from '@angular/common/http';

@Component({
standalone: true,
Expand All @@ -15,11 +15,10 @@ export class AppComponent implements OnInit {
title = 'client';
serverMessage!: string;

constructor(private readonly httpClient: HttpClient) {}
constructor(private readonly defaultRestService: DefaultRestService) { }

ngOnInit() {
this.httpClient
.get<{ message: string }>('http://localhost:3000/api')
this.defaultRestService.appControllerGetData()
.subscribe((result) => (this.serverMessage = result.message));
}
}
12 changes: 9 additions & 3 deletions apps/client/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { RestClientApiModule, RestClientConfiguration } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { appRoutes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(appRoutes),
provideHttpClient(),
importProvidersFrom(RestClientApiModule.forRoot(() => new RestClientConfiguration(
{
basePath: 'http://localhost:3000'
}
)))
],
};
15 changes: 8 additions & 7 deletions apps/server-e2e/src/server/server.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import axios from 'axios';
import { Configuration, DefaultApi } from '@nestjs-mod-fullstack/app-rest-sdk';

describe('GET /api', () => {
const defaultApi = new DefaultApi(new Configuration({ basePath: '/api' }))
let newDemoObject: { id: string };

it('should return a message', async () => {
const res = await axios.get(`/api`);
const res = await defaultApi.appControllerGetData();

expect(res.status).toBe(200);
expect(res.data).toEqual({ message: 'Hello API' });
});

it('should create and return a demo object', async () => {
const res = await axios.post(`/api/demo`);
const res = await defaultApi.appControllerDemoCreateOne();

expect(res.status).toBe(201);
expect(res.data.name).toContain('demo name');
Expand All @@ -20,28 +21,28 @@ describe('GET /api', () => {
});

it('should get demo object by id', async () => {
const res = await axios.get(`/api/demo/${newDemoObject.id}`);
const res = await defaultApi.appControllerDemoFindOne(newDemoObject.id)

expect(res.status).toBe(200);
expect(res.data).toMatchObject(newDemoObject);
});

it('should get all demo object', async () => {
const res = await axios.get(`/api/demo`);
const res = await defaultApi.appControllerDemoFindMany();

expect(res.status).toBe(200);
expect(res.data.filter(row => row.id === newDemoObject.id)).toMatchObject([newDemoObject]);
});

it('should delete demo object by id', async () => {
const res = await axios.delete(`/api/demo/${newDemoObject.id}`);
const res = await defaultApi.appControllerDemoDeleteOne(newDemoObject.id);

expect(res.status).toBe(200);
expect(res.data).toMatchObject(newDemoObject);
});

it('should get all demo object', async () => {
const res = await axios.get(`/api/demo`);
const res = await defaultApi.appControllerDemoFindMany();

expect(res.status).toBe(200);
expect(res.data.filter(row => row.id === newDemoObject.id)).toMatchObject([]);
Expand Down
1 change: 1 addition & 0 deletions apps/server/INFRASTRUCTURE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Static variables of primitive and complex types that are used in the module and
| Key | Description | Constraints | Default | Value |
| ------ | ----------- | ----------- | ------- | ----- |
|`mode`|Mode of start application: init - for run NestJS life cycle, listen - for full start NestJS application|**optional**|```listen```|```silent```|
|`preListen`|Method for additional actions before listening|**optional**|-|```preListen```|

## Infrastructure modules
Infrastructure modules are needed to create configurations that launch various external services (examples: docker-compose file for raising a database, gitlab configuration for deploying an application). Only NestJS-mod compatible modules.
Expand Down
6 changes: 5 additions & 1 deletion apps/server/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@
"executor": "nx:run-commands",
"options": {
"commands": [
"./node_modules/.bin/prisma generate --schema=./apps/server/src/prisma/app-schema.prisma"
"./node_modules/.bin/prisma generate --schema=./apps/server/src/prisma/app-schema.prisma",
"./node_modules/.bin/rucken make-ts-list",
"export NESTJS_MODE=infrastructure && ./node_modules/.bin/nx serve server --host=0.0.0.0 --watch=false",
"rm -rf ./libs/sdk/app-angular-rest-sdk/src/lib && mkdir ./libs/sdk/app-angular-rest-sdk/src/lib && ./node_modules/.bin/openapi-generator-cli generate -i ./app-swagger.json -g typescript-angular -o ./libs/sdk/app-angular-rest-sdk/src/lib --additional-properties=apiModulePrefix=RestClient,configurationPrefix=RestClient,fileNaming=kebab-case,modelFileSuffix=.interface,modelSuffix=Interface,enumNameSuffix=Type,enumPropertyNaming=original,serviceFileSuffix=-rest.service,serviceSuffix=RestService",
"rm -rf ./libs/sdk/app-rest-sdk/src/lib && mkdir ./libs/sdk/app-rest-sdk/src/lib && ./node_modules/.bin/openapi-generator-cli generate -i ./app-swagger.json -g typescript-axios -o ./libs/sdk/app-rest-sdk/src/lib"
],
"parallel": false,
"envFile": "./.env",
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { Controller, Delete, Get, Param, Post } from '@nestjs/common';

import { InjectPrismaClient } from '@nestjs-mod/prisma';
import { ApiCreatedResponse, ApiProperty, ApiResponse } from '@nestjs/swagger';
import { PrismaClient as AppPrismaClient } from '@prisma/app-client';
import { randomUUID } from 'crypto';
import { AppService } from './app.service';
import { AppDemo } from './generated/rest/dto/app_demo';

export class AppData {
@ApiProperty({ type: String })
message: string;
}


@Controller()
export class AppController {
constructor(
Expand All @@ -12,26 +21,31 @@ export class AppController {
private readonly appService: AppService) { }

@Get()
@ApiResponse({ type: AppData })
getData() {
return this.appService.getData();
}

@Post('/demo')
@ApiCreatedResponse({ type: AppDemo })
async demoCreateOne() {
return await this.appPrismaClient.appDemo.create({ data: { name: 'demo name' + randomUUID() } })
}

@Get('/demo/:id')
@ApiResponse({ type: AppDemo })
async demoFindOne(@Param('id') id: string) {
return await this.appPrismaClient.appDemo.findFirstOrThrow({ where: { id } })
}

@Delete('/demo/:id')
@ApiResponse({ type: AppDemo })
async demoDeleteOne(@Param('id') id: string) {
return await this.appPrismaClient.appDemo.delete({ where: { id } })
}

@Get('/demo')
@ApiResponse({ type: AppDemo, isArray: true })
async demoFindMany() {
return await this.appPrismaClient.appDemo.findMany()
}
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/app/generated/rest/dto/app_demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';

export class AppDemo {
@ApiProperty({ type: String })
id: string;

@ApiProperty({ type: String })
name: string;

@ApiProperty({ type: Date })
createdAt: Date;

@ApiProperty({ type: Date })
updatedAt: Date;
}
33 changes: 33 additions & 0 deletions apps/server/src/app/generated/rest/dto/migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class migrations {
@ApiProperty({ type: Number })
installed_rank: number;

@ApiPropertyOptional({ type: String })
version?: string;

@ApiProperty({ type: String })
description: string;

@ApiProperty({ type: String })
type: string;

@ApiProperty({ type: String })
script: string;

@ApiPropertyOptional({ type: Number })
checksum?: number;

@ApiProperty({ type: String })
installed_by: string;

@ApiProperty({ type: Date })
installed_on: Date;

@ApiProperty({ type: Number })
execution_time: number;

@ApiProperty({ type: Boolean })
success: boolean;
}
37 changes: 33 additions & 4 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import {
import { FLYWAY_JS_CONFIG_FILE, Flyway } from '@nestjs-mod/flyway';
import { NestjsPinoLoggerModule } from '@nestjs-mod/pino';
import { ECOSYSTEM_CONFIG_FILE, Pm2 } from '@nestjs-mod/pm2';
import { FakePrismaClient, PRISMA_SCHEMA_FILE, PrismaModule } from '@nestjs-mod/prisma';
import { TerminusHealthCheckModule } from '@nestjs-mod/terminus';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { MemoryHealthIndicator } from '@nestjs/terminus';
import { join } from 'path';
import { writeFileSync } from 'fs';
import { join, resolve } from 'path';
import { AppModule } from './app/app.module';
import { FakePrismaClient, PRISMA_SCHEMA_FILE, PrismaModule } from '@nestjs-mod/prisma';


const appFeatureName = 'app';
const rootFolder = join(__dirname, '..', '..', '..');
Expand Down Expand Up @@ -60,7 +61,35 @@ bootstrapNestApplication({
DefaultNestApplicationListener.forRoot({
staticConfiguration: {
// When running in infrastructure mode, the backend server does not start.
mode: isInfrastructureMode() ? 'silent' : 'listen',
mode: isInfrastructureMode() ? 'silent' : 'listen', async preListen(options) {

if (options.app) {
options.app.setGlobalPrefix('api');

const swaggerConf = new DocumentBuilder()
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(options.app, swaggerConf);

SwaggerModule.setup('swagger', options.app, document);

if (isInfrastructureMode()) {
writeFileSync(
resolve(
__dirname,
'..',
'..',
'..',
'app-swagger.json',
),
JSON.stringify(document),
);
}

}

},
},
}),
],
Expand Down
19 changes: 14 additions & 5 deletions apps/server/src/prisma/app-schema.prisma
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
generator client {
provider = "prisma-client-js"
output = "../../../../node_modules/@prisma/app-client"
provider = "prisma-client-js"
engineType = "binary"
output = "../../../../node_modules/@prisma/app-client"
}

datasource db {
provider = "postgres"
url = env("SERVER_APP_DATABASE_URL")
provider = "postgres"
url = env("SERVER_APP_DATABASE_URL")
}


generator prismaClassGenerator {
provider = "prisma-class-generator"
output = "../app/generated/rest/dto"
dryRun = "false"
separateRelationFields = "false"
makeIndexFile = "file"
}

model AppDemo {
Expand Down
2 changes: 1 addition & 1 deletion ecosystem.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"apps": [
{
"name": "server",
"script": "./node_modules/.bin/nx serve server",
"script": "node ./dist/apps/server/main.js",
"node_args": "-r dotenv/config"
},
{
Expand Down
Loading

0 comments on commit 0353b23

Please sign in to comment.