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

Feature/download api schema #114

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,6 @@ dist
.idea
.DS_Store
website/.yarn

packages/nestjs-swagger-ui/jsonSchema-v4.json
packages/nestjs-swagger-ui/open-api-v3.json
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@
"watch": "yarn build && ./node_modules/.bin/tsc --build --watch",
"lint": "eslint \"./*.json\" \"packages/*/src/**/*.{ts,js,json}\" --fix",
"test": "jest --testTimeout 30000",
"test:one": "jest --testTimeout 30000 packages/nestjs-invitation/src/services/invitation.service.spec.ts",
"test:one": "jest --testTimeout 30000 packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts",
"prepare": "husky install && yarn clean && yarn build",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:ci": "yarn test:cov --ci --reporters=default --reporters=jest-junit",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./jest.config-e2e.json --testTimeout 30000",
"test:e2e:one": "jest --config ./jest.config-e2e.json --testTimeout 30000 packages/nestjs-invitation/src/controllers/invitation.controller.e2e-spec.ts",
"test:e2e:one": "jest --config ./jest.config-e2e.json --testTimeout 30000 packages/nestjs-swagger-ui/src/schema.controller.e2e-spec.ts",
"test:all": "yarn test && yarn test:e2e",
"doc": "rimraf ./documentation && compodoc -p ./tsconfig.doc.json --disablePrivate --disableProtected",
"doc:serve": "yarn doc -s",
Expand Down
13 changes: 11 additions & 2 deletions packages/nestjs-swagger-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@
],
"dependencies": {
"@concepta/nestjs-common": "^4.0.0-alpha.23",
"@concepta/nestjs-typeorm-ext": "^4.0.0-alpha.23",
"@concepta/ts-core": "^4.0.0-alpha.23",
"@concepta/typeorm-common": "^4.0.0-alpha.23",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/swagger": "^6.0.0"
"@nestjs/swagger": "^6.0.0",
"@openapi-contrib/openapi-schema-to-json-schema": "^3.2.0",
"express": "^4.18.2"
},
"devDependencies": {
"@concepta/nestjs-crud": "^4.0.0-alpha.22",
"@concepta/nestjs-user": "^4.0.0-alpha.22",
"@nestjs/core": "^9.0.0",
"@nestjs/testing": "^9.0.0"
"@nestjs/testing": "^9.0.0",
"supertest": "^6.3.1",
"typeorm": "^0.3.10"
}
}
42 changes: 42 additions & 0 deletions packages/nestjs-swagger-ui/src/__fixtures__/user.entity.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { AuditSqlLiteEmbed } from '@concepta/typeorm-common';
import { AuditInterface } from '@concepta/ts-core';

@Entity()
export class UserEntityFixture {
/**
* Unique Id
*/
@PrimaryGeneratedColumn('uuid')
id!: string;

/**
* Email
*/
@Column()
email!: string;

/**
* Username
*/
@Column()
username!: string;

/**
* Password hash
*/
@Column({ type: 'text', nullable: true, default: null })
passwordHash: string | null = null;

/**
* Password salt
*/
@Column({ type: 'text', nullable: true, default: null })
passwordSalt: string | null = null;

/**
* Audit embed
*/
@Column(() => AuditSqlLiteEmbed)
audit!: AuditInterface;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@ export const swaggerUiDefaultConfig = registerAs(
name: process.env.SWAGGER_UI_LICENSE_NAME ?? '',
url: process.env.SWAGGER_UI_LICENSE_URL ?? '',
},
jsonSchemaFilePath:
process.env.SWAGGER_JSON_SCHEMA_FILE_PATH ??
`${__dirname}/../jsonSchema-v4.json`,
openApiFilePath:
process.env.SWAGGER_OPEN_API_FILE_PATH ??
`${__dirname}/../open-api-v3.json`,
}),
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export interface SwaggerUiSettingsInterface {
contact?: { name: string; url: string; email: string };
license?: { name: string; url: string };
basePath?: string;
jsonSchemaFilePath: string;
openApiFilePath: string;
}
76 changes: 76 additions & 0 deletions packages/nestjs-swagger-ui/src/schema.controller.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import supertest from 'supertest';
import { Test } from '@nestjs/testing';
import { UserModule } from '@concepta/nestjs-user';
import { INestApplication } from '@nestjs/common';
import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext';
import { CrudModule } from '@concepta/nestjs-crud';

import { SwaggerUiModule } from './swagger-ui.module';
import { UserEntityFixture } from './__fixtures__/user.entity.fixture';
import { SchemaController } from './schema.controller';
import { SwaggerUiService } from './swagger-ui.service';

describe(SwaggerUiModule, () => {
let app: INestApplication;

const moduleOptions = {
settings: {
path: 'api',
basePath: '/v1',
jsonSchemaFilePath: `${__dirname}/../jsonSchema-v4.json`,
openApiFilePath: `${__dirname}/../open-api-v3.json`,
},
};

describe(SchemaController.name, () => {
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [
TypeOrmExtModule.forRoot({
type: 'sqlite',
database: ':memory:',
synchronize: true,
entities: [UserEntityFixture],
}),
CrudModule.forRoot({}),
UserModule.forRoot({
entities: {
user: {
entity: UserEntityFixture,
},
},
}),
SwaggerUiModule.register(moduleOptions),
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();

const swaggerUiService = app.get(SwaggerUiService);
expect(swaggerUiService).toBeInstanceOf(SwaggerUiService);
swaggerUiService.setup(app);
});

it('download open api schema', async () => {
const response = await supertest(app.getHttpServer())
.get('/schema/open-api-v3')
.expect(200);

const { text } = response;

expect(text).toContain('"openapi": "3.0.0"');
expect(text).toContain('"/user/{id}"');
});

it('download json schema', async () => {
const response = await supertest(app.getHttpServer())
.get('/schema/json-v4')
.expect(200);

const { text } = response;

expect(text).toContain('UserDto');
expect(text).toContain('UserCreateDto');
});
});
});
49 changes: 49 additions & 0 deletions packages/nestjs-swagger-ui/src/schema.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Response } from 'express';
import { Readable } from 'stream';
import { Controller, Get, Res } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';

import { SwaggerUiService } from './swagger-ui.service';

@Controller('schema')
@ApiTags('schema')
export class SchemaController {
constructor(private readonly swaggerUiService: SwaggerUiService) {}

private static readonly JSON_V4 = 'json-v4';
private static readonly OPEN_API_V3 = 'open-api-v3';

@ApiOperation({
summary: 'Download json schema v4',
})
@Get(SchemaController.JSON_V4)
async downloadJsonSchema(@Res() res: Response) {
const file = await this.swaggerUiService.getJsonSchema();
this.sendFile(res, file, SchemaController.JSON_V4);
}

@ApiOperation({
summary: 'Download open api v3',
})
@Get(SchemaController.OPEN_API_V3)
async downloadOpenApi(@Res() res: Response) {
const file = await this.swaggerUiService.getOpenApi();
this.sendFile(res, file, SchemaController.OPEN_API_V3);
}

private sendFile(res: Response, file: Readable, fileName: string) {
res.set(this.getCsvFileHeaders(fileName));

file.pipe(res);
}

private getCsvFileHeaders(filename: string): {
'Content-Disposition': string;
'Content-Type': string;
} {
return {
'Content-Type': 'text/json',
'Content-Disposition': `attachment; filename="${filename}.csv"`,
};
}
}
9 changes: 8 additions & 1 deletion packages/nestjs-swagger-ui/src/swagger-ui.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ describe(SwaggerUiModule, () => {
let swaggerUiService: SwaggerUiService;
let settings: SwaggerUiSettingsInterface;

const moduleOptions = { settings: { path: 'api', basePath: '/v1' } };
const moduleOptions = {
settings: {
path: 'api',
basePath: '/v1',
jsonSchemaFilePath: `${__dirname}/../jsonSchema-v4.json`,
openApiFilePath: `${__dirname}/../open-api-v3.json`,
},
};

describe(SwaggerUiModule.register, () => {
beforeAll(async () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/nestjs-swagger-ui/src/swagger-ui.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { DynamicModule, Module } from '@nestjs/common';

import { SwaggerUiService } from './swagger-ui.service';
import {
SwaggerUiAsyncOptions,
SwaggerUiModuleClass,
SwaggerUiOptions,
} from './swagger-ui.module-definition';
import { SchemaController } from './schema.controller';

@Module({
providers: [SwaggerUiService],
exports: [SwaggerUiService],
controllers: [SchemaController],
})
export class SwaggerUiModule extends SwaggerUiModuleClass {
static register(options: SwaggerUiOptions): DynamicModule {
Expand Down
10 changes: 8 additions & 2 deletions packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';

import { SwaggerUiModule } from './swagger-ui.module';
import { SwaggerUiService } from './swagger-ui.service';

Expand All @@ -11,7 +12,12 @@ describe('SwaggerModule (e2e)', () => {
module = await Test.createTestingModule({
imports: [
SwaggerUiModule.register({
settings: { path: '/api', basePath: '/v1' },
settings: {
path: 'api',
basePath: '/v1',
jsonSchemaFilePath: `${__dirname}/../jsonSchema-v4.json`,
openApiFilePath: `${__dirname}/../open-api-v3.json`,
},
}),
],
}).compile();
Expand All @@ -21,7 +27,7 @@ describe('SwaggerModule (e2e)', () => {
jest.clearAllMocks();
});

it('setup', async () => {
it.only('setup', async () => {
app = module.createNestApplication();
const swaggerUiService = app.get(SwaggerUiService);
expect(swaggerUiService).toBeInstanceOf(SwaggerUiService);
Expand Down
32 changes: 31 additions & 1 deletion packages/nestjs-swagger-ui/src/swagger-ui.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import fs from 'fs';
import { Readable } from 'stream';
import { INestApplication, Inject, Injectable } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import toJsonSchema from '@openapi-contrib/openapi-schema-to-json-schema';

import { SwaggerUiSettingsInterface } from './interfaces/swagger-ui-settings.interface';
import {
SWAGGER_UI_MODULE_SETTINGS_TOKEN,
SWAGGER_UI_MODULE_DOCUMENT_BUILDER_TOKEN,
} from './swagger-ui.constants';
import { bufferToStream, writeObjectInAFile } from './utils/file-utils';

@Injectable()
export class SwaggerUiService {
/**
* Constructor.
*
* @param settings swagger ui settings
* @param documentBuilder
*/
constructor(
@Inject(SWAGGER_UI_MODULE_SETTINGS_TOKEN)
Expand Down Expand Up @@ -47,5 +53,29 @@ export class SwaggerUiService {
document,
this.settings?.customOptions,
);

this.saveApiAndJsonSchemas(document);
}

async getJsonSchema(): Promise<Readable> {
return await this.readFile(this.settings.jsonSchemaFilePath);
}

async getOpenApi(): Promise<Readable> {
return await this.readFile(this.settings.openApiFilePath);
}

async saveApiAndJsonSchemas(document: OpenAPIObject) {
const convertedSchema = toJsonSchema(document);
const { schemas } = convertedSchema?.components ?? {};

writeObjectInAFile(this.settings.openApiFilePath, document);
writeObjectInAFile(this.settings.jsonSchemaFilePath, schemas);
}

private async readFile(filePath: string): Promise<Readable> {
const buffer = await fs.promises.readFile(filePath);

return bufferToStream(buffer);
}
}
15 changes: 15 additions & 0 deletions packages/nestjs-swagger-ui/src/utils/file-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from 'fs';
import { Readable } from 'stream';

export const bufferToStream = (buffer: Buffer): Readable => {
return new Readable({
read() {
this.push(buffer);
this.push(null);
},
});
};

export const writeObjectInAFile = async (filePath: string, obj: unknown) => {
await fs.promises.writeFile(filePath, JSON.stringify(obj, null, 2));
};
Loading