diff --git a/.gitignore b/.gitignore index c1215e062..878786bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/package.json b/package.json index 72104fae1..43dabf524 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/nestjs-swagger-ui/package.json b/packages/nestjs-swagger-ui/package.json index 651c25f3d..69c52647c 100644 --- a/packages/nestjs-swagger-ui/package.json +++ b/packages/nestjs-swagger-ui/package.json @@ -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" } } diff --git a/packages/nestjs-swagger-ui/src/__fixtures__/user.entity.fixture.ts b/packages/nestjs-swagger-ui/src/__fixtures__/user.entity.fixture.ts new file mode 100644 index 000000000..2f64f25ad --- /dev/null +++ b/packages/nestjs-swagger-ui/src/__fixtures__/user.entity.fixture.ts @@ -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; +} diff --git a/packages/nestjs-swagger-ui/src/config/swagger-ui-default.config.ts b/packages/nestjs-swagger-ui/src/config/swagger-ui-default.config.ts index 216c40b3f..f6b5aa069 100644 --- a/packages/nestjs-swagger-ui/src/config/swagger-ui-default.config.ts +++ b/packages/nestjs-swagger-ui/src/config/swagger-ui-default.config.ts @@ -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`, }), ); diff --git a/packages/nestjs-swagger-ui/src/interfaces/swagger-ui-settings.interface.ts b/packages/nestjs-swagger-ui/src/interfaces/swagger-ui-settings.interface.ts index 2a87ce5b3..1c76b89f1 100644 --- a/packages/nestjs-swagger-ui/src/interfaces/swagger-ui-settings.interface.ts +++ b/packages/nestjs-swagger-ui/src/interfaces/swagger-ui-settings.interface.ts @@ -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; } diff --git a/packages/nestjs-swagger-ui/src/schema.controller.e2e-spec.ts b/packages/nestjs-swagger-ui/src/schema.controller.e2e-spec.ts new file mode 100644 index 000000000..d246042e4 --- /dev/null +++ b/packages/nestjs-swagger-ui/src/schema.controller.e2e-spec.ts @@ -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'); + }); + }); +}); diff --git a/packages/nestjs-swagger-ui/src/schema.controller.ts b/packages/nestjs-swagger-ui/src/schema.controller.ts new file mode 100644 index 000000000..df2e15858 --- /dev/null +++ b/packages/nestjs-swagger-ui/src/schema.controller.ts @@ -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"`, + }; + } +} diff --git a/packages/nestjs-swagger-ui/src/swagger-ui.module.spec.ts b/packages/nestjs-swagger-ui/src/swagger-ui.module.spec.ts index 1a9c748d4..cb83bc8e2 100644 --- a/packages/nestjs-swagger-ui/src/swagger-ui.module.spec.ts +++ b/packages/nestjs-swagger-ui/src/swagger-ui.module.spec.ts @@ -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 () => { diff --git a/packages/nestjs-swagger-ui/src/swagger-ui.module.ts b/packages/nestjs-swagger-ui/src/swagger-ui.module.ts index 1ce665beb..012b46a93 100644 --- a/packages/nestjs-swagger-ui/src/swagger-ui.module.ts +++ b/packages/nestjs-swagger-ui/src/swagger-ui.module.ts @@ -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 { diff --git a/packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts b/packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts index 891a5d933..84ba6a7dd 100644 --- a/packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts +++ b/packages/nestjs-swagger-ui/src/swagger-ui.service.spec.ts @@ -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'; @@ -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(); @@ -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); diff --git a/packages/nestjs-swagger-ui/src/swagger-ui.service.ts b/packages/nestjs-swagger-ui/src/swagger-ui.service.ts index 68729d7b5..e31032e6e 100644 --- a/packages/nestjs-swagger-ui/src/swagger-ui.service.ts +++ b/packages/nestjs-swagger-ui/src/swagger-ui.service.ts @@ -1,10 +1,15 @@ +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 { @@ -12,6 +17,7 @@ export class SwaggerUiService { * Constructor. * * @param settings swagger ui settings + * @param documentBuilder */ constructor( @Inject(SWAGGER_UI_MODULE_SETTINGS_TOKEN) @@ -47,5 +53,29 @@ export class SwaggerUiService { document, this.settings?.customOptions, ); + + this.saveApiAndJsonSchemas(document); + } + + async getJsonSchema(): Promise { + return await this.readFile(this.settings.jsonSchemaFilePath); + } + + async getOpenApi(): Promise { + 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 { + const buffer = await fs.promises.readFile(filePath); + + return bufferToStream(buffer); } } diff --git a/packages/nestjs-swagger-ui/src/utils/file-utils.ts b/packages/nestjs-swagger-ui/src/utils/file-utils.ts new file mode 100644 index 000000000..33d133089 --- /dev/null +++ b/packages/nestjs-swagger-ui/src/utils/file-utils.ts @@ -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)); +}; diff --git a/yarn.lock b/yarn.lock index 9f9fa5620..cf4dee874 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2772,6 +2772,13 @@ dependencies: "@octokit/openapi-types" "^12.11.0" +"@openapi-contrib/openapi-schema-to-json-schema@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.2.0.tgz#c4c92edd4478b5ecb3d99c29ecb355118259dccc" + integrity sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw== + dependencies: + fast-deep-equal "^3.1.3" + "@selderee/plugin-htmlparser2@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d" @@ -4204,6 +4211,24 @@ body-parser@1.20.0: type-is "~1.6.18" unpipe "1.0.0" +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -6522,6 +6547,43 @@ express@4.18.1: utils-merge "1.0.1" vary "~1.1.2" +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + ext@^1.1.2: version "1.6.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" @@ -11751,18 +11813,18 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@6.9.3: - version "6.9.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" - integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== - -qs@^6.10.3, qs@^6.8.0, qs@^6.9.4: +qs@6.11.0, qs@^6.10.3, qs@^6.11.0, qs@^6.8.0, qs@^6.9.4: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -13201,6 +13263,22 @@ superagent@^8.0.0: readable-stream "^3.6.0" semver "^7.3.7" +superagent@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.3.tgz#15c8ec5611a1f01386994cfeeda5aa138bcb7b17" + integrity sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + supertest@^6.1.3, supertest@^6.1.6, supertest@^6.2.3: version "6.2.4" resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.4.tgz#3dcebe42f7fd6f28dd7ac74c6cba881f7101b2f0" @@ -13209,6 +13287,14 @@ supertest@^6.1.3, supertest@^6.1.6, supertest@^6.2.3: methods "^1.1.2" superagent "^8.0.0" +supertest@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.1.tgz#a8ad362fc6f323c88730ac191ce30427dc869088" + integrity sha512-hRohNeIfk/cA48Cxpa/w48hktP6ZaRqXb0QV5rLvW0C7paRsBU3Q5zydzYrslOJtj/gd48qx540jKtcs6vG1fQ== + dependencies: + methods "^1.1.2" + superagent "^8.0.3" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -13763,6 +13849,29 @@ typeorm@^0.3.0: xml2js "^0.4.23" yargs "^17.3.1" +typeorm@^0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.10.tgz#aa2857fd4b078c912ca693b7eee01b6535704458" + integrity sha512-VMKiM84EpJQ+Mz9xDIPqnfplWhyUy1d8ccaKdMY9obifxJOTFnv8GYVyPsGwG8Lk7Nb8MlttHyHWENGAhBA3WA== + dependencies: + "@sqltools/formatter" "^1.2.2" + app-root-path "^3.0.0" + buffer "^6.0.3" + chalk "^4.1.0" + cli-highlight "^2.1.11" + date-fns "^2.28.0" + debug "^4.3.3" + dotenv "^16.0.0" + glob "^7.2.0" + js-yaml "^4.1.0" + mkdirp "^1.0.4" + reflect-metadata "^0.1.13" + sha.js "^2.4.11" + tslib "^2.3.1" + uuid "^8.3.2" + xml2js "^0.4.23" + yargs "^17.3.1" + typescript@4.7.4, typescript@^4.0.3, typescript@^4.3.5, typescript@^4.4.3: version "4.7.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"