Comparing version 0.2.3 to 0.3.0-dev.20210923
{ | ||
"name": "nestia", | ||
"version": "0.2.3", | ||
"version": "0.3.0-dev.20210923", | ||
"description": "Automatic SDK and Document generator for the NestJS", | ||
@@ -11,3 +11,3 @@ "main": "src/index.ts", | ||
"dev": "tsc --watch", | ||
"test": "ts-node src/bin/nestia sdk src/test/controllers --out api" | ||
"test": "cd test && bash script.sh" | ||
}, | ||
@@ -14,0 +14,0 @@ "repository": { |
181
README.md
@@ -9,7 +9,2 @@ # Nestia | ||
## Outline | ||
If you're making a backend server with the **TypeScript** and **NestJS**, you don't need any extra dedication, for delivering Rest API to the client (front-end) developers, like writing `swagger.json` comments. | ||
Just generate a SDK library through the **Nestia** and deliver the SDK library to the client developers. The client developers can call your backend server API just by calling the SDK library functions with *await* symbol, re-using the interfaces what you've defined. | ||
```bash | ||
@@ -20,11 +15,37 @@ npm install --save-dev nestia | ||
If you want to see an example project using the **Nestia**, click below links: | ||
Don't write any `swagger` comment. Just deliver the SDK. | ||
- [Controllers of the NestJS](https://github.surf/samchon/nestia/blob/HEAD/src/test/controllers/base/SaleCommentsController.ts) | ||
- [Structures used in the RestAPI](https://github.surf/samchon/nestia/blob/HEAD/api/structures/sales/articles/ISaleArticle.ts) | ||
- [**SDK generated by the Nestia**](https://github.surf/samchon/nestia/blob/HEAD/api/functional/consumers/sales/reviews/index.ts) | ||
When you're developing a backend server using the `NestJS`, you don't need any extra dedication, for delivering the Rest API to the client developers, like writing the `swagger` comments. You just run this **Nestia** up, then **Nestia** would generate the SDK automatically, by analyzing your controller classes in the compliation and runtime level. | ||
With the automatically generated SDK through this **Nestia**, client developer also does not need any extra work, like reading `swagger` and writing the duplicated interaction code. Client developer only needs to import the SDK and calls matched function with the `await` symbol. | ||
```typescript | ||
import api from "@samchon/bbs-api"; | ||
import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; | ||
import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; | ||
export async function test_article_read(connection: api.IConnection): Promise<void> | ||
{ | ||
// LIST UP ARTICLE SUMMARIES | ||
const index: IPage<IBbsArticle.ISummary> = await api.functional.bbs.articles.index | ||
( | ||
connection, | ||
"free", | ||
{ limit: 100, page: 1 } | ||
); | ||
// READ AN ARTICLE DETAILY | ||
const article: IBbsArticle = await api.functional.bbs.articles.at | ||
( | ||
connection, | ||
"free", | ||
index.data[0].id | ||
); | ||
console.log(article.title, aritlce.body, article.files); | ||
} | ||
``` | ||
## Usage | ||
@@ -34,3 +55,2 @@ ### Installation | ||
npm install --save-dev nestia | ||
npx nestia sdk "src/controllers" --out "src/api" | ||
``` | ||
@@ -42,3 +62,3 @@ | ||
### CLI options | ||
### SDK generation | ||
```bash | ||
@@ -51,5 +71,8 @@ npx nestia sdk <source_controller_directory> --out <output_sdk_directory> | ||
To generate a SDK library through the **Nestia** is very easy. Just type the `nestia sdk <input> --out <output>` command in the console. If there're multiple source directories containing the NestJS controller classes, type all of them separating by a `space` word. | ||
To generate a SDK library through the **Nestia** is very easy. | ||
Just type the `nestia sdk <input> --out <output>` command in the console. If there're multiple source directories containing the NestJS controller classes, type all of them separating by a `space` word. | ||
Also, when generating a SDK using the cli options, `compilerOptions` would follow the `tsconfig.json`. If no `tsconfig.json` file exists in your project, the configuration would be the `ES5` with `strict` mode. If you want to use different `compilerOptions` with the `tsconfig.json`, you should configure the [nestia.config.ts](#nestiaconfigts). | ||
```bash | ||
@@ -59,5 +82,135 @@ npx nestia install | ||
Also, SDK library generated by the **Nestia** has some dependencies like below. When you type the `nestia install` command in the console, those dependencies would be automatically install and would be enrolled to the `dependencies` field in the `package.json` | ||
### Dependencies | ||
SDK library generated by the **Nestia** has some dependencies like below. When you type the `nestia install` command in the console, those dependencies would be automatically installed and enrolled to the `dependencies` field in the `package.json` | ||
- [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) | ||
- [node-fetch](https://github.com/node-fetch/node-fetch) | ||
- [node-fetch](https://github.com/node-fetch/node-fetch) | ||
## Advanced | ||
### `NestiaApplication` | ||
```typescript | ||
import tsc from "typescript"; | ||
export class NestiaApplication | ||
{ | ||
public constructor(config: NestiaApplication.IConfiguration); | ||
public generate(): Promise<void>; | ||
public simulate(): Promise<void>; | ||
} | ||
export namespace NestiaApplication | ||
{ | ||
export interface IConfiguration | ||
{ | ||
/** | ||
* List of directories containing the NestJS controller classes. | ||
*/ | ||
input: string[]; | ||
/** | ||
* Output directory that SDK would be placed in. | ||
*/ | ||
output: string; | ||
/** | ||
* Compiler options for the TypeScript. | ||
* | ||
* If omitted, the configuration would follow the `tsconfig.json`. | ||
*/ | ||
compilerOptions?: tsc.CompilerOptions | ||
} | ||
} | ||
``` | ||
### `nestia.config.ts` | ||
### Recommended Structures | ||
## Demonstration | ||
- [Controllers of the NestJS](https://github.surf/samchon/nestia/blob/HEAD/src/test/controllers/base/SaleCommentsController.ts) | ||
- [Structures used in the RestAPI](https://github.surf/samchon/nestia/blob/HEAD/api/structures/sales/articles/ISaleArticle.ts) | ||
- [SDK generated by this **Nestia**](https://github.surf/samchon/nestia/blob/HEAD/api/functional/consumers/sales/reviews/index.ts) | ||
### Controller | ||
If you've decided to adapt this **Nestia** and you want to generate the SDK directly, you don't need any extra work. Just keep you controller class down and do noting. The only one exceptional case that you need an extra dedication is, when you want to explain about the API function to the client developers through the comments. | ||
```typescript | ||
@nest.Controller("consumers/:section/sales/:saleId/questions") | ||
export class ConsumerSaleQuestionsController | ||
{ | ||
/** | ||
* Store a new question. | ||
* | ||
* @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 question | ||
* @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 | ||
*/ | ||
@nest.Post() | ||
public store | ||
( | ||
@nest.Request() request: express.Request, | ||
@nest.Param("section") section: string, | ||
@nest.Param("saleId") saleId: number, | ||
@nest.Body() input: ISaleQuestion.IStore | ||
): Promise<ISaleQuestion>; | ||
} | ||
``` | ||
### SDK | ||
When you run the **Nestia** up using the upper controller class `ConsumerSaleQuestionsController`, the **Nestia** would generate below function for the client developers, by analyzing the `ConsumerSaleQuestionsController` class in the compilation and runtime level. | ||
As you can see, the comments from the `ConsumerSaleQuestionsController.store()` are fully copied to the SDK function. Therefore, if you want to deliver detailed description about the API function, writing the detailed comment would be tne best choice. | ||
```typescript | ||
/** | ||
* Store a new question. | ||
* | ||
* @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 question | ||
* @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 | ||
* | ||
* @nestia Generated by Nestia - https://github.com/samchon/nestia | ||
* @controller ConsumerSaleQuestionsController.store() | ||
* @path POST /consumers/:section/sales/:saleId/questions/ | ||
*/ | ||
export function store | ||
( | ||
connection: IConnection, | ||
section: string, | ||
saleId: number, | ||
input: store.Input | ||
): Promise<store.Output> | ||
{ | ||
return Fetcher.fetch | ||
( | ||
connection, | ||
{ | ||
input_encrypted: false, | ||
output_encrypted: false | ||
}, | ||
"POST", | ||
`/consumers/${section}/sales/${saleId}/questions/`, | ||
input | ||
); | ||
} | ||
export namespace store | ||
{ | ||
export type Input = Primitive<ISaleInquiry.IStore>; | ||
export type Output = Primitive<ISaleInquiry<ISaleArticle.IContent>>; | ||
} | ||
``` |
@@ -6,38 +6,73 @@ #!/usr/bin/env ts-node-script | ||
import path from "path"; | ||
import tsc from "typescript"; | ||
import { NestiaApplication } from "../NestiaApplication"; | ||
import { Terminal } from "../utils/Terminal"; | ||
import { stripJsonComments } from "../utils/stripJsonComments"; | ||
import { NestiaApplication } from "../NestiaApplication"; | ||
interface ICommand | ||
{ | ||
install: boolean; | ||
out: string | null; | ||
} | ||
async function sdk(inputList: string[], command: ICommand): Promise<void> | ||
async function sdk(input: string[], command: ICommand): Promise<void> | ||
{ | ||
// VALIDATE OUTPUT | ||
let compilerOptions: tsc.CompilerOptions | undefined = {}; | ||
//---- | ||
// NESTIA.CONFIG.TS | ||
//---- | ||
if (fs.existsSync("nestia.config.ts") === true) | ||
{ | ||
const config: NestiaApplication.IConfiguration = await import(path.resolve("nestia.config.ts")); | ||
compilerOptions = config.compilerOptions; | ||
input = config.input; | ||
command.out = config.output; | ||
} | ||
//---- | ||
// VALIDATIONS | ||
//---- | ||
// CHECK OUTPUT | ||
if (command.out === null) | ||
throw new Error(`Output directory is not specified. Add the "--out <output_directory>" option.`); | ||
// CHECK PARENT DIRECTORY | ||
const parentPath: string = path.resolve(command.out + "/.."); | ||
const parentStats: fs.Stats = await fs.promises.stat(parentPath); | ||
if (parentStats.isDirectory() === false) | ||
throw new Error(`Unable to find parent directory of the output path: "${parentPath}".`); | ||
// VALIDATE INPUTS | ||
for (const input of inputList) | ||
// CHECK INPUTS | ||
for (const path of input) | ||
{ | ||
const inputStats: fs.Stats = await fs.promises.stat(input); | ||
const inputStats: fs.Stats = await fs.promises.stat(path); | ||
if (inputStats.isDirectory() === false) | ||
throw new Error(`Target "${inputList}" is not a directory.`); | ||
throw new Error(`Target "${path}" is not a directory.`); | ||
} | ||
//---- | ||
// GENERATE | ||
// GENERATION | ||
//---- | ||
// CALL THE APP.SDK() | ||
const app: NestiaApplication = new NestiaApplication(inputList, command.out); | ||
await app.sdk(); | ||
if (fs.existsSync("tsconfig.json") === true) | ||
{ | ||
const content: string = await fs.promises.readFile("tsconfig.json", "utf8"); | ||
const options: tsc.CompilerOptions = JSON.parse(stripJsonComments(content)).compilerOptions; | ||
compilerOptions = compilerOptions | ||
? { ...options, ...compilerOptions } | ||
: options; | ||
} | ||
// CHECK NESTIA.CONFIG.TS | ||
// CALL THE APP.GENERATE() | ||
const app: NestiaApplication = new NestiaApplication({ | ||
output: command.out, | ||
input, | ||
compilerOptions, | ||
}); | ||
await app.generate(); | ||
} | ||
@@ -62,3 +97,2 @@ | ||
out: ["o", "Output path of the SDK files", "string", null], | ||
install: ["i", "Install Dependencies", "boolean", false] | ||
}); | ||
@@ -65,0 +99,0 @@ |
@@ -17,2 +17,3 @@ import type * as tsc from "typescript"; | ||
.map(closure => closure(route, query, input)) | ||
.filter(str => !!str) | ||
.join("\n"); | ||
@@ -47,7 +48,8 @@ } | ||
// RETURNS WITH FINALIZATION | ||
return "" | ||
return "{\n" | ||
+ " return Fetcher.fetch\n" | ||
+ " (\n" | ||
+ fetchArguments.map(param => ` ${param}`).join(",\n") + "\n" | ||
+ " );"; | ||
+ " );\n" | ||
+ "}"; | ||
} | ||
@@ -147,7 +149,6 @@ | ||
+ `${parameters.map(str => ` ${str}`).join(",\n")}\n` | ||
+ ` ): Promise<${output}>\n` | ||
+ "{"; | ||
+ ` ): Promise<${output}>`; | ||
} | ||
function tail(route: IRoute, query: IRoute.IParameter | undefined, input: IRoute.IParameter | undefined): string | ||
function tail(route: IRoute, query: IRoute.IParameter | undefined, input: IRoute.IParameter | undefined): string | null | ||
{ | ||
@@ -163,6 +164,5 @@ const types: Pair<string, string>[] = []; | ||
if (types.length === 0) | ||
return "}"; | ||
return null; | ||
return `}\n` | ||
+ `export namespace ${route.name}\n` | ||
return `export namespace ${route.name}\n` | ||
+ "{\n" | ||
@@ -169,0 +169,0 @@ + (types.map(tuple => ` export type ${tuple.first} = Primitive<${tuple.second}>;`).join("\n")) + "\n" |
@@ -18,16 +18,8 @@ import * as fs from "fs"; | ||
{ | ||
public readonly inputs: string[]; | ||
public readonly output: string; | ||
private readonly config_: NestiaApplication.IConfiguration; | ||
private readonly bundle_checker_: Singleton<Promise<(str: string) => boolean>>; | ||
public constructor | ||
( | ||
inputs: string[], | ||
output: string | ||
) | ||
public constructor(config: NestiaApplication.IConfiguration) | ||
{ | ||
this.inputs = inputs.map(str => path.resolve(str)); | ||
this.output = path.resolve(output); | ||
this.config_ = config; | ||
this.bundle_checker_ = new Singleton(async () => | ||
@@ -38,3 +30,3 @@ { | ||
{ | ||
const relative: string = `${this.output}${path.sep}${file}`; | ||
const relative: string = `${config.output}${path.sep}${file}`; | ||
const stats: fs.Stats = await fs.promises.stat(`${__dirname}${path.sep}bundle${path.sep}${file}`); | ||
@@ -57,9 +49,9 @@ | ||
public async sdk(): Promise<void> | ||
public async generate(): Promise<void> | ||
{ | ||
// LOAD CONTROLLER FILES | ||
const fileList: string[] = []; | ||
for (const input of this.inputs) | ||
for (const file of this.config_.input.map(str => path.resolve(str))) | ||
{ | ||
const found: string[] = await SourceFinder.find(input); | ||
const found: string[] = await SourceFinder.find(file); | ||
const filtered: string[] = await ArrayUtil.asyncFilter(found, file => this.is_not_excluded(file)); | ||
@@ -78,3 +70,7 @@ | ||
// ANALYZE TYPESCRIPT CODE | ||
const program: tsc.Program = tsc.createProgram(controllerList.map(c => c.file), {}); | ||
const program: tsc.Program = tsc.createProgram | ||
( | ||
controllerList.map(c => c.file), | ||
this.config_.compilerOptions || {} | ||
); | ||
const checker: tsc.TypeChecker = program.getTypeChecker(); | ||
@@ -93,3 +89,3 @@ | ||
// DO GENERATE | ||
await SdkGenerator.generate(this.output, routeList); | ||
await SdkGenerator.generate(this.config_.output, routeList); | ||
} | ||
@@ -99,5 +95,15 @@ | ||
{ | ||
return file.indexOf(`${this.output}${path.sep}functional`) === -1 | ||
return file.indexOf(`${this.config_.output}${path.sep}functional`) === -1 | ||
&& (await this.bundle_checker_.get())(file) === false; | ||
} | ||
} | ||
export namespace NestiaApplication | ||
{ | ||
export interface IConfiguration | ||
{ | ||
input: string[]; | ||
output: string; | ||
compilerOptions?: tsc.CompilerOptions; | ||
} | ||
} |
@@ -76,3 +76,3 @@ { | ||
}, | ||
"include": ["src", "api/structures"] | ||
// "include": ["src", "test"] | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
75144
30
1566
210