Skip to content
/ nestia Public
forked from samchon/nestia

Automatic SDK and Swagger generator for the NestJS

License

Notifications You must be signed in to change notification settings

yiy0ung/nestia

 
 

Repository files navigation

Nestia

Automatic SDK and Swagger generator for the NestJS, evolved than ever.

GitHub license npm version Downloads Build Status Guide Documents

nestia is an evolved SDK and Swagger generator, which analyzes your NestJS server code in the compilation level. With nestia and compilation level analyzer, you don't need to write any swagger or class-validator decorators. All you need to do is use the nestia CLI as shown below.

Reading below contents, feel how the "compilation level" makes nestia stronger.

Components nestia::SDK nestia::swagger @nestjs/swagger
Pure DTO interface
Description comments
Simple structure
Generic type
Union type
Intersection type
Conditional type
Auto completion
Type hints
5x faster JSON.stringify()
Ensure type safety
// IMPORT SDK LIBRARY GENERATED BY NESTIA
import api from "@samchon/shopping-api";
import { IPage } from "@samchon/shopping-api/lib/structures/IPage";
import { ISale } from "@samchon/shopping-api/lib/structures/ISale";
import { ISaleQuestion } from "@samchon/shopping-api/lib/structures/ISaleQuestion";

export async function trace_sale_question_and_comment
    (connection: api.IConnection): Promise<void>
{
    // LIST UP SALE SUMMARIES
    const index: IPage<ISale.ISummary> = await api.functional.shoppings.sales.index
    (
        connection,
        "general",
        { limit: 100, page: 1 }
    );

    // PICK A SALE
    const sale: ISale = await api.functional.shoppings.sales.at
    (
        connection, 
        "general",
        index.data[0].id
    );
    console.log("sale", sale);

    // WRITE A QUESTION
    const question: ISaleQuestion = await api.functional.shoppings.sales.questions.store
    (
        connection,
        "general",
        sale.id,
        {
            title: "How to use this product?",
            body: "The description is not fully enough. Can you introduce me more?",
            files: []
        }
    );
    console.log("question", question);
}

Setup

Boilerplate Project

npx nestia start <directory>

Just run the upper command, then boilerplate project using nestia would be installed.

When the installation has been completed, you can start NestJS backend development directly. and you also can generate SDK library or Swagger documents by running below command. You can get more information by reading README content of the boilderplate project.

cd <directory>
npm run build:sdk
npm run build:swagger

However, supported range of the boilerplate project is limited. It guides from "setting up nestia configuration" to "how to accomplish TDD through SDK", but nothing more. If you want more guidance like configuring DB or preparing non-distruptive update system, visit samchon/backend and create a new repository from that.

Manual Installation

If you need to manual install, follow below step.

At first, install those dependencies.

npm install --save nestia-helper
npm install --save-dev nestia

npm install --save-dev typescript
npm install --save-dev ttypescript
npm install --save-dev ts-node

After the installation, check the tsconfig.json file. When you're using nestia, your TypeScript project must be configured to using CommonJS module with strict mode. Also, if you configure plugins like below to using nestia-helper, you can get great benefit by using pure DTO interface.

{
  "compilerOptions": {
    "module": "CommonJS",
    "strict": true,
    "plugins": [
      { "transform": "nestia-helper/lib/transform" } 
    ]
  }
}

When all manual setup processes are completed, you can generate the SDK or Swagger.

npx nestia sdk "src/**/*.controller" --out "src/api"
npx nestia swagger "src/**/*.controller" --out "swagger.json"

If all of your controller files are gathered into one directory:

npx nestia sdk "src/controllers" --out "src/api"
npx nestia swagger "src/controllers" --out "swagger.json"

You can omit all of the parameters if you've configured the nestia.config.ts file.

npx nestia sdk
npx nestia swagger

Demonstrations

Pure DTO Interface

nestia can utilize pure interface type as DTO.

Unlike @nestjs/swagger which requires the DTO class with decorators, nestia can use the pure interface type directly. Also, nestia can utilize the pure descriptive comments, instead of using the description property of the decorators. Furthermore, nestia can even support generic types, union/intersection types and even conditional types.

Look at the code below, you may see the difference between nestia and @nestjs/swagger, and thereby catch the meaning of the pure DTO interface.

/**
 * Comment wrote on a sale related article.
 * 
 * When an article of a sale has been enrolled, all of the participants like consumers and
 * sellers can write a comment on that article. However, when the writer is a consumer, the
 * consumer can hide its name through the annoymous option. 
 * 
 * Also, writing a reply comment for a specific comment is possible and in that case, the 
 * {@link ISaleArticleComment.parent_id} property would be activated.
 * 
 * @author Jeongho Nam - https://github.com/samchon
 */
export interface ISaleArticleComment
{
    /**
     * Primary Key.
     */
    id: number;

    /**
     * Parent comment ID.
     * 
     * Only When this comment has been written as a reply.
     */
    parent_id: number | null;

    /**
     * Type of the writer.
     */
    writer_type: "seller" | "consumer";

    /**
     * Name of the writer.
     * 
     * When this is a type of anonymous comment, writer name would be hidden.
     */
    writer_name: string | null;

    /**
     * Contents of the comments.
     * 
     * When the comment writer tries to modify content, it would not modify the comment
     * content but would be accumulated. Therefore, all of the people can read how
     * the content has been changed.
     */
    contents: ISaleArticleComment.IContent[];

    /**
     * Creation time.
     */
    created_at: string;
}

Advanced Controller Class

Controller also can use the generic arguments.

In the previous Pure DTO Interface corner, we've learned that nestia can use the pure interface type as DTO. Also, we've learned that utilizing generic, union/intersection and even conditional typed interfaces are also possible.

In the Controller case, it's same with the upper DTO story. With nestia, defining a generic typed controller class is also possible, too. By defining a generic typed controller class as a super-type class, you can reduce both duplicated code and description comments.

Look at the below code and feel how powerful nestia is.

Also, you can validate request body data from client automatically, by using nestia-helper and its TypedBody() decorator, which is maximum 1,000x times faster than other. Furthermore, nestia-helper boosts up JSON string conversion speed about 5x times faster through its TypedRoute() component.

typescript-json benchmark

import express from "express";
import { Controller, Param, Request } from "@nestjs/common";
import { TypedBody, TypedRoute } from "nestia-helper";

import { ISaleArticleComment } from "../api/structures/ISaleArticleComment";

@Controller("consumers/:section/sales/:saleId/articles/:articleId/comments")
export class ConsumerSaleArticleCommentsController {
    /**
     * Store a new comment.
     *
     * Write a comment on a sale article. If you configure the comment to be
     * `anonymous`, only administrator, you and seller of the sale can read
     * the content.
     *
     * @param request Instance of the Express.Request
     * @param sectionCode Code of the target section
     * @param saleId ID of the target sale
     * @param articleId ID of the target article
     * @param body Content to write
     * @return Newly archived comment
     *
     * @throw 400 bad request error when type of the input data is not valid
     * @throw 401 unauthorized error when you've not logged in yet
     * @throw 403 forbidden error when you're a seller and the sale is not yours
     * @throw 404 not found error when unable to find the matched record
     */
    @TypedRoute.Post() // 5x faster JSON.stringify()
    public async store(
        @Request() request: express.Request,
        @Param("section") sectionCode: string,
        @Param("saleId") saleId: string,
        @Param("articleId") articleId: string,
        @TypedBody() body: ISaleArticleComment.IStore, // auto validation
    ): Promise<ISaleArticleComment>;
}

Software Development Kit

Swagger is torturing client developers.

If you're a backend developer and you deliver a Swagger to your companion client developers, they should analyze the Swagger and implement duplicated router functions with DTO interfaces by themselves. During those jobs, if a client developer takes a mistake by mis-reading the Swagger, it becomes a critical runtime error directly.

Why are you torturing the client developers such like that? If you deliver an SDK (Software Development Kit) instead of the Swagger, the client developers don't need to read the Swagger file. They never need to implement the duplicated DTO interfaces with router functions, either.

Therefore, just build the SDK through this nestia and deliver the SDK. Your client developers would be anticipated from the long time torturing and become happy. Your solution would be much more reliable and efficient, too.

Looking at the SDK library file, generated by nestia, it is perfect.

Route method, path and parameters are well-formed and DTO structures are correctly imported. Also, descriptive comments are fully revived in the SDK library, regardless of where they are written.

Furthermore, there's not any problem even when a generic typed controller class comes. nestia will specialize the generic arguments exactly, by analyzing your NestJS server code, in the compilation level.

/**
 * @packageDocumentation
 * @module api.functional.consumers.sales.reviews
 * @nestia Generated by Nestia - https://github.com/samchon/nestia 
 */
//================================================================
import { Fetcher, Primitive } from "nestia-fetcher";
import type { IConnection } from "nestia-fetcher";
import TSON from "typescript-json";

import type { ISaleReview } from "./../../../../structures/ISaleReview";
import type { ISaleInquiry } from "./../../../../structures/ISaleInquiry";

/**
 * Store a new inquiry.
 * 
 * Write a new article inquirying about a sale.
 * 
 * @param connection connection Information of the remote HTTP(s) server with headers (+encryption password)
 * @param request Instance of the Express.Request
 * @param section Code of the target section
 * @param saleId ID of the target sale
 * @param input Content to archive
 * @return Newly archived inquiry
 * @throw 400 bad request error when type of the input data is not valid
 * @throw 401 unauthorized error when you've not logged in yet
 * 
 * @controller ConsumerSaleReviewsController.store()
 * @path POST /consumers/:section/sales/:saleId/reviews
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export function store
    (
        connection: IConnection,
        section: string,
        saleId: string,
        input: Primitive<store.Input>
    ): Promise<store.Output>
{
    return Fetcher.fetch
    (
        connection,
        store.ENCRYPTED,
        store.METHOD,
        store.path(section, saleId),
        input,
        store.stringify
    );
}
export namespace store
{
    export type Input = Primitive<ISaleReview.IStore>;
    export type Output = Primitive<ISaleInquiry<ISaleReview.IContent>>;

    export const METHOD = "POST" as const;
    export const PATH: string = "/consumers/:section/sales/:saleId/reviews";
    export const ENCRYPTED: Fetcher.IEncrypted = {
        request: false,
        response: false,
    };

    export function path(section: string, saleId: string): string
    {
        return `/consumers/${section}/sales/${saleId}/reviews`;
    }
    export const stringify = (input: Input) => TSON.stringify(input);
}

/**
 * Update an inquiry.
 * 
 * Update ordinary inquiry article. However, it would not modify the content reocrd
 * {@link ISaleInquiry.IContent}, but be accumulated into the {@link ISaleInquiry.contents}. 
 * Therefore, all of the poeple can read how the content has been changed.
 * 
 * @param connection connection Information of the remote HTTP(s) server with headers (+encryption password)
 * @param request Instance of the Express.Request
 * @param section Code of the target section
 * @param saleId ID of the target sale
 * @param id ID of the target article to be updated
 * @param input New content to be overwritten
 * @return The newly created content record
 * @throw 400 bad request error when type of the input data is not valid
 * @throw 401 unauthorized error when you've not logged in yet
 * @throw 403 forbidden error when the article is not yours
 * 
 * @controller ConsumerSaleReviewsController.update()
 * @path PUT /consumers/:section/sales/:saleId/reviews/:id
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export function update
    (
        connection: IConnection,
        section: string,
        saleId: string,
        id: number,
        input: Primitive<update.Input>
    ): Promise<update.Output>
{
    return Fetcher.fetch
    (
        connection,
        update.ENCRYPTED,
        update.METHOD,
        update.path(section, saleId, id),
        input,
        update.stringify
    );
}
export namespace update
{
    export type Input = Primitive<ISaleReview.IStore>;
    export type Output = Primitive<ISaleInquiry<ISaleReview.IContent>>;

    export const METHOD = "PUT" as const;
    export const PATH: string = "/consumers/:section/sales/:saleId/reviews/:id";
    export const ENCRYPTED: Fetcher.IEncrypted = {
        request: false,
        response: false,
    };

    export function path(section: string, saleId: string, id: number): string
    {
        return `/consumers/${section}/sales/${saleId}/reviews/${id}`;
    }
    export const stringify = (input: Input) => TSON.stringify(input);
}

Swagger

Building Swagger is also possible and even much powerful.

Looking at the simple/swagger.json file, generated by nestia, everything is perfect. Route method, path and parameters are well-formed. Also, schema definitions are exactly matched with the pure interface type ISaleArticleComment. Of course, descriptive comments are perfectly resurrected in the description properties of the swagger.json file.

Looking at the another file generic/swagger.json, you can find that there isn't any problem even when a generic typed DTO and controller come. The last file union/swagger.json, there's no problem on the union type, either.

Swagger Editor

Configuration

Components nestia.config.ts CLI @nestjs/swagger
Swagger Generation
SDK Generation
5x faster JSON.stringify()
Type check in runtime
Custom compiler options

nestia can configure generator options by two ways: CLI and configuration file.

At first, the CLI (Command Line Interface) is convenient, but does not support detailed options.

# BASIC COMMAND
npx nestia <sdk|swagger> <source_directories_or_patterns> \
    --exclude <exclude_directory_or_pattern> \
    --out <output_directory_or_file>

# EXAMPLES
npx nestia sdk "src/controllers" --out "src/api"
npx nestia swagger "src/**/*.controller.ts" --out "swagger.json"
npx nestia swagger "src/main/controllers" "src/sub/controllers" \
    --exclude "src/main/test" \
    --out "composite.swagger.json"

# ONLY WHEN NESTIA.CONFIG.TS EXISTS
npx nestia sdk
npx nestia swagger

Besides, the configuration file nestia.config.ts supports much detailed options.

The detailed options are listed up to the IConfiguration interface. You can utilize the IConfiguration type like below. If you want to know more about those options, please check the Guide Documents.

Read IConfiguration
import ts from "typescript";
import type { StripEnums } from "./utils/StripEnums";

/**
 * Definition for the `nestia.config.ts` file.
 *
 * @author Jeongho Nam - https://github.com/samchon
 */
export interface IConfiguration {
    /**
     * List of files or directories containing the NestJS controller classes.
     */
    input: string | string[] | IConfiguration.IInput;

    /**
     * Output directory that SDK would be placed in.
     *
     * If not configured, you can't build the SDK library.
     */
    output?: string;

    /**
     * Compiler options for the TypeScript.
     *
     * If you've omitted this property or the assigned property cannot fully cover the
     * `tsconfig.json`, the properties from the `tsconfig.json` would be assigned to here.
     * Otherwise, this property has been configured and it's detailed values are different
     * with the `tsconfig.json`, this property values would be used instead.
     *
     * ```typescript
     * import ts from "typescript";
     *
     * const tsconfig: ts.TsConfig;
     * const nestiaConfig: IConfiguration;
     *
     * const compilerOptions: ts.CompilerOptions = {
     *     ...tsconfig.compilerOptions,
     *     ...(nestiaConfig.compilerOptions || {})
     * }
     * ```
     */
    compilerOptions?: StripEnums<ts.CompilerOptions>;

    /**
     * Whether to assert parameter types or not.
     *
     * If you configure this property to be `true`, all of the function parameters would be
     * checked through the [typescript-json](https://github.com/samchon/typescript-json#runtime-type-checkers).
     * This option would make your SDK library slower, but would enahcne the type safety even
     * in the runtime level.
     *
     * @default false
     */
    assert?: boolean;

    /**
     * Whether to optimize JSON string conversion 2x faster or not.
     *
     * If you configure this property to be `true`, the SDK library would utilize the
     * [typescript-json](https://github.com/samchon/typescript-json#fastest-json-string-converter)
     * and the JSON string conversion speed really be 2x faster.
     *
     * @default false
     */
    json?: boolean;

    /**
     * Whether to wrap DTO by primitive type.
     *
     * If you don't configure this property as `false`, all of DTOs in the
     * SDK library would be automatically wrapped by {@link Primitive} type.
     *
     * For refenrece, if a DTO type be capsuled by the {@link Primitive} type,
     * all of methods in the DTO type would be automatically erased. Also, if
     * the DTO has a `toJSON()` method, the DTO type would be automatically
     * converted to return type of the `toJSON()` method.
     *
     * @default true
     */
    primitive?: boolean;

    /**
     * Building `swagger.json` is also possible.
     *
     * If not specified, you can't build the `swagger.json`.
     */
    swagger?: IConfiguration.ISwagger;
}
export namespace IConfiguration {
    /**
     * List of files or directories to include or exclude to specifying the NestJS
     * controllers.
     */
    export interface IInput {
        /**
         * List of files or directories containing the NestJS controller classes.
         */
        include: string[];

        /**
         * List of files or directories to be excluded.
         */
        exclude?: string[];
    }

    /**
     * Building `swagger.json` is also possible.
     */
    export interface ISwagger {
        /**
         * Output path of the `swagger.json`.
         *
         * If you've configured only directory, the file name would be the `swagger.json`.
         * Otherwise you've configured the full path with file name and extension, the
         * `swagger.json` file would be renamed to it.
         */
        output: string;
    }
}
import type { IConfiguration } from "nestia";

export const NESTIA_CONFIG: IConfiguration = {
    input: "./src/controllers",
    output: "./src/api",
    json: true,
    swagger: {
        output: "./public/swagger.json"
    }
};
export default NESTIA_CONFIG;

Appendix

Dependencies of the SDK

An SDK library generated by nestia requires nestia-fetcher module. Also, typescript-json module can be required following your nestia.config.ts configuration file.

The npx nestia install command installs those dependencies with the package.json configuration.

# MOVE TO THE DISTRIBUTION DIRECTORY
cd packages/api

# INSTALL DEPENDENCIES OF THE SDK
npx nestia install

Nestia-Helper

https://github.com/samchon/nestia-helper

If you utilize nestia with nestia-helper, you can automatically validatea pure DTO interace without any extra dedication. It analyzes your backend server code in the compilation level and add request body validation code automatically.

import helper from "nestia-helper";
import { Controller } from "@nestjs/common";

@Controller("bbs/articles")
export class BbsArticlesController {
    //----
    // `TSON.stringify()` for `IBbsArticle` 
    // Boost up JSON conversion speed about 5x times faster 
    //----
    // `TSON.assert()` for `IBbsArticle.IStore`
    // If client request body is not following type type, 
    // `BadRequestException` (status code: 400) would be thrown
    //----
    @helper.TypedRoute.Post()
    public async store(
        // automatic validation
        @helper.TypedBody() input: IBbsArticle.IStore
    ): Promise<IBbsArticle> {
        const article: BbsArticle = await BbsArticeProvider.store(input);
        const json: IBbsArticle = await BbsArticleProvider.json().getOne(article);

        // 5x times faster JSON conversion
        return Paginator.paginate(stmt, input);
    }
}

TypeScript-JSON

https://github.com/samchon/typescript-json

import TSON from "typescript-json";

//----
// RUNTIME VALIDATORS
//----
// ALLOW SUPERFLUOUS PROPERTIES
TSON.assert<T>(input); // throws exception
TSON.is<T>(input); // returns boolean value
TSON.validate<T>(input); // archives all errors

// DO NOT ALLOW SUPERFLUOUS PROPERTIES
TSON.equals<T>(input); // returns boolean value
TSON.assert<T>(input); // throws exception
TSON.validateEquals<T>(input); // archives all errors

//----
// APPENDIX FUNCTIONS
//----
TSON.stringify<T>(input); // 5x faster JSON.stringify()
TSON.application<[T, U, V], "swagger">(); // JSON schema application generator
TSON.create<T>(input); // 2x faster object creator (only one-time construction)

typescript-json is a transformer library providing JSON related functions.

  • Powerful Runtime type checkers:
    • Performed by only one line, TSON.assert<T>(input)
    • Only one library which can validate union type
    • Maximum 9,000x faster than other libraries
  • 5x faster JSON.stringify() function:
    • Performed by only one line: TSON.stringify<T>(input)
    • Only one library which can stringify union type
    • 10,000x faster optimizer construction time than similar libraries

typescript-json analyzes TypeScript source code and generates optimized validators and JSON string converters in the compilation level. It is the reason why nestia can generate SDK library is by analyzing NestJS code and how nestia-helper validates request body data automatically.

Also, its performance enhancement is much greater than other libraries. For example, validator function TSON.is<T>(input: T): boolean is maximum 9,000x times faster than other validator libraries.

About

Automatic SDK and Swagger generator for the NestJS

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 96.6%
  • JavaScript 3.4%