@nestia/migrate 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/NestiaMigrateApplication.js +280 -151
- package/lib/NestiaMigrateApplication.js.map +1 -1
- package/lib/archivers/FileArchiver.js +0 -1
- package/lib/archivers/FileArchiver.js.map +1 -1
- package/lib/bundles/TEMPLATE.js +15 -5
- package/lib/bundles/TEMPLATE.js.map +1 -1
- package/lib/executable/bundle.js +0 -1
- package/lib/executable/bundle.js.map +1 -1
- package/lib/programmers/ControllerProgrammer.js +1 -1
- package/lib/programmers/ControllerProgrammer.js.map +1 -1
- package/lib/programmers/RouteProgrammer.js +70 -35
- package/lib/programmers/RouteProgrammer.js.map +1 -1
- package/lib/structures/IMigrateRoute.d.ts +3 -0
- package/lib/structures/ISwagger.d.ts +2 -6
- package/lib/structures/ISwaggerInfo.d.ts +71 -0
- package/lib/structures/ISwaggerInfo.js +3 -0
- package/lib/structures/ISwaggerInfo.js.map +1 -0
- package/lib/structures/ISwaggerRoute.d.ts +5 -3
- package/package.json +4 -4
- package/src/NestiaMigrateApplication.ts +22 -1
- package/src/archivers/FileArchiver.ts +0 -1
- package/src/bundles/TEMPLATE.ts +15 -5
- package/src/executable/bundle.ts +0 -1
- package/src/programmers/ControllerProgrammer.ts +1 -1
- package/src/programmers/RouteProgrammer.ts +88 -35
- package/src/structures/IMigrateRoute.ts +4 -0
- package/src/structures/ISwagger.ts +3 -7
- package/src/structures/ISwaggerInfo.ts +80 -0
- package/src/structures/ISwaggerRoute.ts +6 -3
@@ -0,0 +1,71 @@
|
|
1
|
+
/**
|
2
|
+
* Information about the API.
|
3
|
+
*
|
4
|
+
* @author Samchon
|
5
|
+
*/
|
6
|
+
export interface ISwaggerInfo {
|
7
|
+
/**
|
8
|
+
* The title of the API.
|
9
|
+
*/
|
10
|
+
title: string;
|
11
|
+
/**
|
12
|
+
* A short description of the API.
|
13
|
+
*/
|
14
|
+
description?: string;
|
15
|
+
/**
|
16
|
+
* A URL to the Terms of Service for the API.
|
17
|
+
*
|
18
|
+
* @format url
|
19
|
+
*/
|
20
|
+
termsOfService?: string;
|
21
|
+
/**
|
22
|
+
* The contact information for the exposed API.
|
23
|
+
*/
|
24
|
+
contact?: ISwaggerInfo.IContact;
|
25
|
+
/**
|
26
|
+
* The license information for the exposed API.
|
27
|
+
*/
|
28
|
+
license?: ISwaggerInfo.ILicense;
|
29
|
+
/**
|
30
|
+
* Version of the API.
|
31
|
+
*/
|
32
|
+
version: string;
|
33
|
+
}
|
34
|
+
export declare namespace ISwaggerInfo {
|
35
|
+
/**
|
36
|
+
* Contact information for the exposed API.
|
37
|
+
*/
|
38
|
+
interface IContact {
|
39
|
+
/**
|
40
|
+
* The identifying name of the contact person/organization.
|
41
|
+
*/
|
42
|
+
name?: string;
|
43
|
+
/**
|
44
|
+
* The URL pointing to the contact information.
|
45
|
+
*
|
46
|
+
* @format url
|
47
|
+
*/
|
48
|
+
url?: string;
|
49
|
+
/**
|
50
|
+
* The email address of the contact person/organization.
|
51
|
+
*
|
52
|
+
* @format email
|
53
|
+
*/
|
54
|
+
email?: string;
|
55
|
+
}
|
56
|
+
/**
|
57
|
+
* License information for the exposed API.
|
58
|
+
*/
|
59
|
+
interface ILicense {
|
60
|
+
/**
|
61
|
+
* The license name used for the API.
|
62
|
+
*/
|
63
|
+
name: string;
|
64
|
+
/**
|
65
|
+
* A URL to the license used for the API.
|
66
|
+
*
|
67
|
+
* @format url
|
68
|
+
*/
|
69
|
+
url?: string;
|
70
|
+
}
|
71
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"ISwaggerInfo.js","sourceRoot":"","sources":["../../src/structures/ISwaggerInfo.ts"],"names":[],"mappings":""}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { IJsDocTagInfo } from "typia/lib/metadata/IJsDocTagInfo";
|
1
2
|
import { ISwaggerSchema } from "./ISwaggeSchema";
|
2
3
|
export interface ISwaggerRoute {
|
3
4
|
parameters?: ISwaggerRoute.IParameter[];
|
@@ -8,10 +9,11 @@ export interface ISwaggerRoute {
|
|
8
9
|
deprecated?: boolean;
|
9
10
|
security?: Record<string, string[]>[];
|
10
11
|
tags?: string[];
|
12
|
+
"x-nestia-jsDocTags"?: IJsDocTagInfo[];
|
11
13
|
}
|
12
14
|
export declare namespace ISwaggerRoute {
|
13
15
|
interface IParameter {
|
14
|
-
name
|
16
|
+
name?: string;
|
15
17
|
in: "path" | "query" | "header" | "cookie";
|
16
18
|
schema: ISwaggerSchema;
|
17
19
|
required?: boolean;
|
@@ -20,7 +22,7 @@ export declare namespace ISwaggerRoute {
|
|
20
22
|
interface IRequestBody {
|
21
23
|
description?: string;
|
22
24
|
content: IContent;
|
23
|
-
required?:
|
25
|
+
required?: boolean;
|
24
26
|
"x-nestia-encrypted"?: boolean;
|
25
27
|
}
|
26
28
|
type IResponseBody = Record<string, {
|
@@ -30,7 +32,7 @@ export declare namespace ISwaggerRoute {
|
|
30
32
|
}>;
|
31
33
|
interface IContent {
|
32
34
|
"text/plain"?: {
|
33
|
-
schema: ISwaggerSchema
|
35
|
+
schema: ISwaggerSchema;
|
34
36
|
};
|
35
37
|
"application/json"?: {
|
36
38
|
schema: ISwaggerSchema;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@nestia/migrate",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"description": "Migration program from swagger to NestJS",
|
5
5
|
"main": "lib/index.js",
|
6
6
|
"typings": "lib/index.d.ts",
|
@@ -30,8 +30,8 @@
|
|
30
30
|
},
|
31
31
|
"homepage": "https://github.com/samchon/nestia#readme",
|
32
32
|
"devDependencies": {
|
33
|
-
"@nestia/core": "^1.
|
34
|
-
"@nestia/fetcher": "^1.
|
33
|
+
"@nestia/core": "^1.5.1",
|
34
|
+
"@nestia/fetcher": "^1.5.1",
|
35
35
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
36
36
|
"@types/node": "^20.3.3",
|
37
37
|
"prettier": "^2.8.8",
|
@@ -45,7 +45,7 @@
|
|
45
45
|
"typescript-transform-paths": "^3.4.6"
|
46
46
|
},
|
47
47
|
"dependencies": {
|
48
|
-
"typia": "^4.1.
|
48
|
+
"typia": "^4.1.13"
|
49
49
|
},
|
50
50
|
"files": [
|
51
51
|
"lib",
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import cp from "child_process";
|
2
|
+
|
1
3
|
import typia from "typia";
|
2
4
|
|
3
5
|
import { FileArchiver } from "./archivers/FileArchiver";
|
@@ -37,7 +39,26 @@ export class NestiaMigrateApplication {
|
|
37
39
|
(output: string): void => {
|
38
40
|
const program: IMigrateProgram = this.analyze();
|
39
41
|
const files: IMigrateFile[] = MigrateProgrammer.write(program);
|
40
|
-
|
42
|
+
|
43
|
+
try {
|
44
|
+
cp.execSync(
|
45
|
+
`git clone https://github.com/samchon/nestia-template "${output}"`,
|
46
|
+
{ stdio: "ignore" },
|
47
|
+
);
|
48
|
+
for (const path of [
|
49
|
+
"/.git",
|
50
|
+
"/src/api",
|
51
|
+
"/src/controllers",
|
52
|
+
"/src/providers",
|
53
|
+
"/test/features",
|
54
|
+
])
|
55
|
+
cp.execSync(`rm -rf "${output}${path}"`, {
|
56
|
+
stdio: "ignore",
|
57
|
+
});
|
58
|
+
} catch {
|
59
|
+
FileArchiver.archive(archiver)(output)(TEMPLATE);
|
60
|
+
}
|
61
|
+
FileArchiver.archive(archiver)(output)(files);
|
41
62
|
};
|
42
63
|
}
|
43
64
|
export namespace NestiaMigrateApplication {
|
@@ -10,7 +10,6 @@ export namespace FileArchiver {
|
|
10
10
|
(operator: IOperator) =>
|
11
11
|
(output: string) =>
|
12
12
|
(files: IMigrateFile[]): void => {
|
13
|
-
operator.mkdir(output);
|
14
13
|
const visited: Set<string> = new Set();
|
15
14
|
for (const f of files) {
|
16
15
|
mkdir(operator.mkdir)(output)(visited)(f.location);
|
package/src/bundles/TEMPLATE.ts
CHANGED
@@ -27,7 +27,7 @@ export const TEMPLATE = [
|
|
27
27
|
{
|
28
28
|
"location": "/.vscode",
|
29
29
|
"file": "launch.json",
|
30
|
-
"content": "{\r\n // Use IntelliSense to learn about possible Node.js debug attributes.\r\n // Hover to view descriptions of existing attributes.\r\n // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\r\n \"version\": \"0.2.0\",\r\n \"configurations\": [\r\n {\r\n \"type\": \"node\",\r\n \"request\": \"launch\",\r\n \"name\": \"
|
30
|
+
"content": "{\r\n // Use IntelliSense to learn about possible Node.js debug attributes.\r\n // Hover to view descriptions of existing attributes.\r\n // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\r\n \"version\": \"0.2.0\",\r\n \"configurations\": [\r\n {\r\n \"type\": \"node\",\r\n \"request\": \"launch\",\r\n \"name\": \"Backend Test\",\r\n \"program\": \"${workspaceRoot}/test/index.ts\",\r\n \"cwd\": \"${workspaceRoot}\",\r\n \"args\": [\r\n // //----\r\n // // Not possible to reset DB in debugging mode\r\n // //\r\n // // Therefore, if you need DB reset, then do it \r\n // // through `npm run reset-for-debugging` command\r\n // //----\r\n // \"--reset\", \"false\",\r\n // \"--mode\", \"local\",\r\n\r\n //----\r\n // You can run specific test functions\r\n //\r\n // If you want to include or exclude multiple words,\r\n // then separate them with space character\r\n //----\r\n // \"--include\", \"some-words-to-include\",\r\n // \"--exclude\", \"some-word another-word\",\r\n ],\r\n \"outFiles\": [\"${workspaceRoot}/bin/**/*.js\"],\r\n }\r\n ]\r\n}"
|
31
31
|
},
|
32
32
|
{
|
33
33
|
"location": "/.vscode",
|
@@ -42,12 +42,12 @@ export const TEMPLATE = [
|
|
42
42
|
{
|
43
43
|
"location": "",
|
44
44
|
"file": "nestia.config.ts",
|
45
|
-
"content": "// nestia configuration file\r\nimport type sdk from \"@nestia/sdk\";\r\n\r\nconst NESTIA_CONFIG: sdk.INestiaConfig = {\r\n input: \"src/controllers\",\r\n output: \"src/api\",\r\n swagger: {\r\n output: \"
|
45
|
+
"content": "// nestia configuration file\r\nimport type sdk from \"@nestia/sdk\";\r\n\r\nconst NESTIA_CONFIG: sdk.INestiaConfig = {\r\n input: \"src/controllers\",\r\n output: \"src/api\",\r\n swagger: {\r\n output: \"packages/api/swagger.json\",\r\n servers: [\r\n {\r\n url: \"http://localhost:37001\",\r\n description: \"Local Server\",\r\n },\r\n ],\r\n },\r\n primitive: false,\r\n simulate: true,\r\n e2e: \"test\",\r\n};\r\nexport default NESTIA_CONFIG;\r\n"
|
46
46
|
},
|
47
47
|
{
|
48
48
|
"location": "",
|
49
49
|
"file": "package.json",
|
50
|
-
"content": "{\r\n \"private\": true,\r\n \"name\": \"@ORGANIZATION/PROJECT\",\r\n \"version\": \"0.1.0\",\r\n \"description\": \"Starter kit of Nestia\",\r\n \"main\": \"lib/index.js\",\r\n \"scripts\": {\r\n \"----------------------------------------------\": \"\",\r\n \"build\": \"npm run build:sdk && npm run build:main && npm run build:test\",\r\n \"build:api\": \"rimraf packages/api/lib && npm run build:sdk && tsc -p packages/api/tsconfig.json\",\r\n \"build:main\": \"rimraf lib && tsc\",\r\n \"build:sdk\": \"rimraf src/api/functional && nestia sdk\",\r\n \"build:swagger\": \"npx nestia swagger\",\r\n \"build:test\": \"rimraf bin && tsc -p test/tsconfig.json\",\r\n \"dev\": \"npm run build:test -- --watch\",\r\n \"eslint\": \"eslint src && eslint test\",\r\n \"eslint:fix\": \"eslint --fix src && eslint --fix test\",\r\n \"prepare\": \"ts-patch install\",\r\n \"prettier\": \"prettier src --write && prettier test --write\",\r\n \"-----------------------------------------------\": \"\",\r\n \"start\": \"node lib/executable/server\",\r\n \"test\": \"node bin/test\",\r\n \"------------------------------------------------\": \"\"\r\n },\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"https://github.com/samchon/nestia-template\"\r\n },\r\n \"keywords\": [\r\n \"nestia\",\r\n \"template\",\r\n \"boilerplate\"\r\n ],\r\n \"author\": \"AUTHOR\",\r\n \"license\": \"MIT\",\r\n \"bugs\": {\r\n \"url\": \"https://github.com/samchon/nestia-template/issues\"\r\n },\r\n \"homepage\": \"https://github.com/samchon/nestia-template#readme\",\r\n \"devDependencies\": {\r\n \"@nestia/e2e\": \"^0.3.6\",\r\n \"@nestia/sdk\": \"^1.
|
50
|
+
"content": "{\r\n \"private\": true,\r\n \"name\": \"@ORGANIZATION/PROJECT\",\r\n \"version\": \"0.1.0\",\r\n \"description\": \"Starter kit of Nestia\",\r\n \"main\": \"lib/index.js\",\r\n \"scripts\": {\r\n \"----------------------------------------------\": \"\",\r\n \"build\": \"npm run build:sdk && npm run build:main && npm run build:test\",\r\n \"build:api\": \"rimraf packages/api/lib && npm run build:sdk && tsc -p packages/api/tsconfig.json\",\r\n \"build:main\": \"rimraf lib && tsc\",\r\n \"build:sdk\": \"rimraf src/api/functional && nestia sdk\",\r\n \"build:swagger\": \"npx nestia swagger\",\r\n \"build:test\": \"rimraf bin && tsc -p test/tsconfig.json\",\r\n \"dev\": \"npm run build:test -- --watch\",\r\n \"eslint\": \"eslint src && eslint test\",\r\n \"eslint:fix\": \"eslint --fix src && eslint --fix test\",\r\n \"package:api\": \"npm run build:swagger && npm run build:api && cd packages/api && npm publish\",\r\n \"prepare\": \"ts-patch install\",\r\n \"prettier\": \"prettier src --write && prettier test --write\",\r\n \"-----------------------------------------------\": \"\",\r\n \"start\": \"node lib/executable/server\",\r\n \"test\": \"node bin/test\",\r\n \"------------------------------------------------\": \"\"\r\n },\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"https://github.com/samchon/nestia-template\"\r\n },\r\n \"keywords\": [\r\n \"nestia\",\r\n \"template\",\r\n \"boilerplate\"\r\n ],\r\n \"author\": \"AUTHOR\",\r\n \"license\": \"MIT\",\r\n \"bugs\": {\r\n \"url\": \"https://github.com/samchon/nestia-template/issues\"\r\n },\r\n \"homepage\": \"https://github.com/samchon/nestia-template#readme\",\r\n \"devDependencies\": {\r\n \"@nestia/e2e\": \"^0.3.6\",\r\n \"@nestia/sdk\": \"^1.5.1\",\r\n \"@trivago/prettier-plugin-sort-imports\": \"^3.3.1\",\r\n \"@types/cli\": \"^0.11.21\",\r\n \"@types/inquirer\": \"^8.2.5\",\r\n \"@types/node\": \"^18.11.0\",\r\n \"@types/uuid\": \"^8.3.4\",\r\n \"@typescript-eslint/eslint-plugin\": \"^5.40.0\",\r\n \"@typescript-eslint/parser\": \"^5.40.0\",\r\n \"chalk\": \"^4.1.0\",\r\n \"cli\": \"^1.0.1\",\r\n \"eslint-plugin-deprecation\": \"^1.4.1\",\r\n \"nestia\": \"^4.3.2\",\r\n \"prettier\": \"^2.7.1\",\r\n \"rimraf\": \"^3.0.2\",\r\n \"source-map-support\": \"^0.5.21\",\r\n \"ts-node\": \"^10.9.1\",\r\n \"ts-patch\": \"^3.0.0\",\r\n \"typescript\": \"^5.1.3\",\r\n \"typescript-transform-paths\": \"^3.4.6\"\r\n },\r\n \"dependencies\": {\r\n \"@nestia/core\": \"^1.5.1\",\r\n \"serialize-error\": \"^4.1.0\",\r\n \"tstl\": \"^2.5.13\",\r\n \"typia\": \"^4.1.13\",\r\n \"uuid\": \"^9.0.0\"\r\n },\r\n \"stackblitz\": {\r\n \"startCommand\": \"npm run prepare && npm run build:test && npm run test\"\r\n }\r\n}\r\n"
|
51
51
|
},
|
52
52
|
{
|
53
53
|
"location": "/packages/api",
|
@@ -57,13 +57,18 @@ export const TEMPLATE = [
|
|
57
57
|
{
|
58
58
|
"location": "/packages/api",
|
59
59
|
"file": "package.json",
|
60
|
-
"content": "{\r\n \"name\": \"@ORGANIZATION/PROJECT-api\",\r\n \"version\": \"0.0.0\",\r\n \"description\": \"API for PROJECT\",\r\n \"main\": \"lib/index.js\",\r\n \"typings\": \"lib/index.d.ts\",\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"https://github.com/samchon/nestia-template\"\r\n },\r\n \"author\": \"AUTHOR\",\r\n \"license\": \"MIT\",\r\n \"bugs\": {\r\n \"url\": \"https://github.com/samchon/nestia-template/issues\"\r\n },\r\n \"homepage\": \"https://github.com/samchon/nestia-template#readme\",\r\n \"dependencies\": {\r\n \"@nestia/fetcher\": \"^1.
|
60
|
+
"content": "{\r\n \"name\": \"@ORGANIZATION/PROJECT-api\",\r\n \"version\": \"0.0.0\",\r\n \"description\": \"API for PROJECT\",\r\n \"main\": \"lib/index.js\",\r\n \"typings\": \"lib/index.d.ts\",\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"https://github.com/samchon/nestia-template\"\r\n },\r\n \"author\": \"AUTHOR\",\r\n \"license\": \"MIT\",\r\n \"bugs\": {\r\n \"url\": \"https://github.com/samchon/nestia-template/issues\"\r\n },\r\n \"homepage\": \"https://github.com/samchon/nestia-template#readme\",\r\n \"dependencies\": {\r\n \"@nestia/fetcher\": \"^1.5.1\",\r\n \"typia\": \"^4.1.13\"\r\n }\r\n}\r\n"
|
61
61
|
},
|
62
62
|
{
|
63
63
|
"location": "/packages/api",
|
64
64
|
"file": "README.md",
|
65
65
|
"content": "# SDK for Client Developers\r\n## Outline\r\n[`@ORGANIZATION/PROJECT`](https://github.com/samchon/nestia-template) provides SDK (Software Development Kit) for convenience.\r\n\r\nFor the client developers who are connecting to this backend server, [`@ORGANIZATION/PROJECT`](https://github.com/samchon/nestia-template) provides not API documents like the Swagger, but provides the API interaction library, one of the typical SDK (Software Development Kit) for the convenience.\r\n\r\nWith the SDK, client developers never need to re-define the duplicated API interfaces. Just utilize the provided interfaces and asynchronous functions defined in the SDK. It would be much convenient than any other Rest API solutions.\r\n\r\n```bash\r\nnpm install --save @ORGANIZATION/PROJECT-api\r\n```\r\n\r\n\r\n\r\n\r\n## Usage\r\nImport the `@ORGANIZATION/PROJECT-api` and enjoy the auto-completion.\r\n\r\n```typescript\r\nimport api from \"@ORGINIZATION/PROJECT-api\";\r\n\r\nimport { IBbsArticle } from \"@ORGANIZATION/PROJECT-api/lib/structures/bbs/IBbsArticle\";\r\n\r\nasync function main(): Promise<void>\r\n{\r\n //----\r\n // PREPARATIONS\r\n //----\r\n // CONNECTION INFO\r\n const connection: api.IConnection = {\r\n host: \"http://127.0.0.1:37001\",\r\n };\r\n\r\n const article: IBbsArticle = await api.functional.bbs.articles.store(\r\n connection,\r\n \"general\",\r\n {\r\n writer: \"Robot\",\r\n title: \"Hello, world!\",\r\n body: \"Hello, I'm test automation robot\",\r\n format: \"txt\",\r\n files: [\r\n {\r\n name: \"logo\",\r\n extension: \"png\",\r\n url: \"https://somewhere.com/logo.png\",\r\n },\r\n ],\r\n password: \"1234\",\r\n },\r\n );\r\n typia.assertEquals(stored);\r\n\r\n const page: IPage<IBbsArticle> = await api.functional.bbs.articles.index(\r\n connection,\r\n \"general\",\r\n {\r\n limit: 100,\r\n search: {\r\n writer: \"Robot\"\r\n }\r\n }\r\n );\r\n await typia.assertEquals(page);\r\n}\r\n```"
|
66
66
|
},
|
67
|
+
{
|
68
|
+
"location": "/packages/api",
|
69
|
+
"file": "swagger.json",
|
70
|
+
"content": "{\r\n \"openapi\": \"3.0.1\",\r\n \"servers\": [\r\n {\r\n \"url\": \"http://localhost:37001\",\r\n \"description\": \"Local Server\"\r\n }\r\n ],\r\n \"info\": {\r\n \"version\": \"0.1.0\",\r\n \"title\": \"@ORGANIZATION/PROJECT\",\r\n \"description\": \"Starter kit of Nestia\",\r\n \"license\": {\r\n \"name\": \"MIT\"\r\n }\r\n },\r\n \"paths\": {\r\n \"/bbs/articles/{section}\": {\r\n \"patch\": {\r\n \"tags\": [],\r\n \"parameters\": [\r\n {\r\n \"name\": \"section\",\r\n \"in\": \"path\",\r\n \"description\": \"Target section\",\r\n \"schema\": {\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n }\r\n ],\r\n \"requestBody\": {\r\n \"description\": \"Pagination request info with searching and sorting options\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.IRequest\"\r\n }\r\n }\r\n },\r\n \"required\": true,\r\n \"x-nestia-encrypted\": false\r\n },\r\n \"responses\": {\r\n \"201\": {\r\n \"description\": \"Paged articles witb summarization\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IPage_lt_IBbsArticle.ISummary_gt_\"\r\n }\r\n }\r\n },\r\n \"x-nestia-encrypted\": false\r\n }\r\n },\r\n \"summary\": \"List up entire articles, but paginated and summarized\",\r\n \"description\": \"List up entire articles, but paginated and summarized.\\n\\nThis method is for listing up summarized articles with pagination.\\n\\nIf you want, you can search and sort articles with specific conditions.\",\r\n \"x-nestia-namespace\": \"bbs.articles.index\",\r\n \"x-nestia-jsDocTags\": [\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"section\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target section\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"input\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Pagination request info with searching and sorting options\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"returns\",\r\n \"text\": [\r\n {\r\n \"text\": \"Paged articles witb summarization\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-nestia-method\": \"PATCH\"\r\n },\r\n \"post\": {\r\n \"tags\": [],\r\n \"parameters\": [\r\n {\r\n \"name\": \"section\",\r\n \"in\": \"path\",\r\n \"description\": \"Target section\",\r\n \"schema\": {\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n }\r\n ],\r\n \"requestBody\": {\r\n \"description\": \"New article info\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.IStore\"\r\n }\r\n }\r\n },\r\n \"required\": true,\r\n \"x-nestia-encrypted\": false\r\n },\r\n \"responses\": {\r\n \"201\": {\r\n \"description\": \"Newly created article info\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle\"\r\n }\r\n }\r\n },\r\n \"x-nestia-encrypted\": false\r\n }\r\n },\r\n \"summary\": \"Store a new article\",\r\n \"description\": \"Store a new article.\\n\\nStore a new article and returns its detailed record info.\",\r\n \"x-nestia-namespace\": \"bbs.articles.store\",\r\n \"x-nestia-jsDocTags\": [\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"section\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target section\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"input\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"New article info\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"returns\",\r\n \"text\": [\r\n {\r\n \"text\": \"Newly created article info\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-nestia-method\": \"POST\"\r\n }\r\n },\r\n \"/bbs/articles/{section}/{id}\": {\r\n \"get\": {\r\n \"tags\": [],\r\n \"parameters\": [\r\n {\r\n \"name\": \"section\",\r\n \"in\": \"path\",\r\n \"description\": \"Target section\",\r\n \"schema\": {\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n },\r\n {\r\n \"name\": \"id\",\r\n \"in\": \"path\",\r\n \"description\": \"Target articles id\",\r\n \"schema\": {\r\n \"format\": \"uuid\",\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n }\r\n ],\r\n \"responses\": {\r\n \"200\": {\r\n \"description\": \"Detailed article info\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle\"\r\n }\r\n }\r\n },\r\n \"x-nestia-encrypted\": false\r\n }\r\n },\r\n \"summary\": \"Get an article with detailed info\",\r\n \"description\": \"Get an article with detailed info.\\n\\nOpen an article with detailed info, increasing reading count.\",\r\n \"x-nestia-namespace\": \"bbs.articles.at\",\r\n \"x-nestia-jsDocTags\": [\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"section\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target section\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"id\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target articles id\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"returns\",\r\n \"text\": [\r\n {\r\n \"text\": \"Detailed article info\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-nestia-method\": \"GET\"\r\n },\r\n \"put\": {\r\n \"tags\": [],\r\n \"parameters\": [\r\n {\r\n \"name\": \"section\",\r\n \"in\": \"path\",\r\n \"description\": \"Target section\",\r\n \"schema\": {\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n },\r\n {\r\n \"name\": \"id\",\r\n \"in\": \"path\",\r\n \"description\": \"Target articles id\",\r\n \"schema\": {\r\n \"format\": \"uuid\",\r\n \"type\": \"string\"\r\n },\r\n \"required\": true\r\n }\r\n ],\r\n \"requestBody\": {\r\n \"description\": \"Content to update\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.IUpdate\"\r\n }\r\n }\r\n },\r\n \"required\": true,\r\n \"x-nestia-encrypted\": false\r\n },\r\n \"responses\": {\r\n \"201\": {\r\n \"description\": \"Newly created content info\",\r\n \"content\": {\r\n \"application/json\": {\r\n \"schema\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.ISnapshot\"\r\n }\r\n }\r\n },\r\n \"x-nestia-encrypted\": false\r\n }\r\n },\r\n \"summary\": \"Update article\",\r\n \"description\": \"Update article.\\n\\nWhen updating, this BBS system does not overwrite the content, but accumulate it.\\nTherefore, whenever an article being updated, length of {@link IBbsArticle.snapshots }\\nwould be increased and accumulated.\",\r\n \"x-nestia-namespace\": \"bbs.articles.update\",\r\n \"x-nestia-jsDocTags\": [\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"section\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target section\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"id\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Target articles id\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"param\",\r\n \"text\": [\r\n {\r\n \"text\": \"input\",\r\n \"kind\": \"parameterName\"\r\n },\r\n {\r\n \"text\": \" \",\r\n \"kind\": \"space\"\r\n },\r\n {\r\n \"text\": \"Content to update\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"returns\",\r\n \"text\": [\r\n {\r\n \"text\": \"Newly created content info\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-nestia-method\": \"PUT\"\r\n }\r\n }\r\n },\r\n \"components\": {\r\n \"schemas\": {\r\n \"IBbsArticle.IRequest\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"search\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.IRequest.ISearch\"\r\n },\r\n \"sort\": {\r\n \"$ref\": \"#/components/schemas/IPage.Sort_lt_IBbsArticle.IRequest.SortableColumns_gt_\"\r\n },\r\n \"page\": {\r\n \"description\": \"Page number.\",\r\n \"x-typia-required\": false,\r\n \"x-typia-optional\": true,\r\n \"type\": \"number\"\r\n },\r\n \"limit\": {\r\n \"description\": \"Limitation of records per a page.\",\r\n \"x-typia-required\": false,\r\n \"x-typia-optional\": true,\r\n \"type\": \"number\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"description\": \"Page request info with some options.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle.IRequest.ISearch\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"writer\": {\r\n \"x-typia-required\": false,\r\n \"x-typia-optional\": true,\r\n \"type\": \"string\"\r\n },\r\n \"title\": {\r\n \"x-typia-required\": false,\r\n \"x-typia-optional\": true,\r\n \"type\": \"string\"\r\n },\r\n \"body\": {\r\n \"x-typia-required\": false,\r\n \"x-typia-optional\": true,\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"description\": \"Searching options.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IPage.Sort_lt_IBbsArticle.IRequest.SortableColumns_gt_\": {\r\n \"description\": \"Sorting column specialization.\\n\\nThe plus means ascending order and the minus means descending order.\",\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"description\": \"Sorting column specialization.\\n\\nThe plus means ascending order and the minus means descending order.\",\r\n \"type\": \"string\",\r\n \"enum\": [\r\n \"-writer\",\r\n \"-title\",\r\n \"-created_at\",\r\n \"-updated_at\",\r\n \"+writer\",\r\n \"+title\",\r\n \"+created_at\",\r\n \"+updated_at\"\r\n ]\r\n }\r\n },\r\n \"IPage_lt_IBbsArticle.ISummary_gt_\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"pagination\": {\r\n \"$ref\": \"#/components/schemas/IPage.IPagination\"\r\n },\r\n \"data\": {\r\n \"description\": \"List of records.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.ISummary\"\r\n }\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"pagination\",\r\n \"data\"\r\n ],\r\n \"description\": \"A page.\\n\\nCollection of records with pagination indformation.\",\r\n \"x-typia-jsDocTags\": [\r\n {\r\n \"name\": \"author\",\r\n \"text\": [\r\n {\r\n \"text\": \"Samchon\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"IPage.IPagination\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"current\": {\r\n \"description\": \"Current page number.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"number\"\r\n },\r\n \"limit\": {\r\n \"description\": \"Limitation of records per a page.\",\r\n \"x-typia-jsDocTags\": [\r\n {\r\n \"name\": \"default\",\r\n \"text\": [\r\n {\r\n \"text\": \"100\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"number\",\r\n \"default\": 100\r\n },\r\n \"records\": {\r\n \"description\": \"Count of total records in database.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"number\"\r\n },\r\n \"pages\": {\r\n \"description\": \"Number of total pages.\\n\\nEqual to {@link records } / {@link limit } with ceiling.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"number\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"current\",\r\n \"limit\",\r\n \"records\",\r\n \"pages\"\r\n ],\r\n \"description\": \"Page information.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle.ISummary\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"id\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"writer\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"title\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"created_at\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"updated_at\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"id\",\r\n \"writer\",\r\n \"title\",\r\n \"created_at\",\r\n \"updated_at\"\r\n ],\r\n \"description\": \"Summarized info.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"id\": {\r\n \"description\": \"Primary Key.\",\r\n \"x-typia-metaTags\": [\r\n {\r\n \"kind\": \"format\",\r\n \"value\": \"uuid\"\r\n }\r\n ],\r\n \"x-typia-jsDocTags\": [\r\n {\r\n \"name\": \"format\",\r\n \"text\": [\r\n {\r\n \"text\": \"uuid\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"format\": \"uuid\"\r\n },\r\n \"section\": {\r\n \"description\": \"Section code.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"writer\": {\r\n \"description\": \"Name of nickname of writer.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"snapshots\": {\r\n \"description\": \"List of snapshot contents.\\n\\nWhenever updating an article, its contents would be accumulated.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"$ref\": \"#/components/schemas/IBbsArticle.ISnapshot\"\r\n }\r\n },\r\n \"created_at\": {\r\n \"description\": \"Creation time of the article.\",\r\n \"x-typia-metaTags\": [\r\n {\r\n \"kind\": \"format\",\r\n \"value\": \"datetime\"\r\n }\r\n ],\r\n \"x-typia-jsDocTags\": [\r\n {\r\n \"name\": \"format\",\r\n \"text\": [\r\n {\r\n \"text\": \"date-time\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"format\": \"date-time\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"id\",\r\n \"section\",\r\n \"writer\",\r\n \"snapshots\",\r\n \"created_at\"\r\n ],\r\n \"description\": \"BBS article.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle.ISnapshot\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"id\": {\r\n \"description\": \"Primary key of individual content.\",\r\n \"x-typia-metaTags\": [\r\n {\r\n \"kind\": \"format\",\r\n \"value\": \"uuid\"\r\n }\r\n ],\r\n \"x-typia-jsDocTags\": [\r\n {\r\n \"name\": \"format\",\r\n \"text\": [\r\n {\r\n \"text\": \"uuid\",\r\n \"kind\": \"text\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"format\": \"uuid\"\r\n },\r\n \"created_at\": {\r\n \"description\": \"Creation time of this content.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"title\": {\r\n \"description\": \"Title of the article.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"body\": {\r\n \"description\": \"Content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"format\": {\r\n \"description\": \"Format of the content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"enum\": [\r\n \"md\",\r\n \"html\",\r\n \"txt\"\r\n ]\r\n },\r\n \"files\": {\r\n \"description\": \"List of files (to be) attached.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"$ref\": \"#/components/schemas/IAttachmentFile\"\r\n }\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"id\",\r\n \"created_at\",\r\n \"title\",\r\n \"body\",\r\n \"format\",\r\n \"files\"\r\n ],\r\n \"description\": \"Content info.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IAttachmentFile\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"name\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"extension\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"nullable\": true\r\n },\r\n \"url\": {\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"name\",\r\n \"extension\",\r\n \"url\"\r\n ],\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle.IStore\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"writer\": {\r\n \"description\": \"Name or nickname of the writer.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"title\": {\r\n \"description\": \"Title of the article.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"body\": {\r\n \"description\": \"Content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"format\": {\r\n \"description\": \"Format of the content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"enum\": [\r\n \"md\",\r\n \"html\",\r\n \"txt\"\r\n ]\r\n },\r\n \"files\": {\r\n \"description\": \"List of files (to be) attached.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"$ref\": \"#/components/schemas/IAttachmentFile\"\r\n }\r\n },\r\n \"password\": {\r\n \"description\": \"Password of the article.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"writer\",\r\n \"title\",\r\n \"body\",\r\n \"format\",\r\n \"files\",\r\n \"password\"\r\n ],\r\n \"description\": \"Store info.\",\r\n \"x-typia-jsDocTags\": []\r\n },\r\n \"IBbsArticle.IUpdate\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"title\": {\r\n \"description\": \"Title of the article.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"body\": {\r\n \"description\": \"Content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n },\r\n \"format\": {\r\n \"description\": \"Format of the content body.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\",\r\n \"enum\": [\r\n \"md\",\r\n \"html\",\r\n \"txt\"\r\n ]\r\n },\r\n \"files\": {\r\n \"description\": \"List of files (to be) attached.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"array\",\r\n \"items\": {\r\n \"$ref\": \"#/components/schemas/IAttachmentFile\"\r\n }\r\n },\r\n \"password\": {\r\n \"description\": \"Password of the article.\",\r\n \"x-typia-required\": true,\r\n \"x-typia-optional\": false,\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"nullable\": false,\r\n \"required\": [\r\n \"title\",\r\n \"body\",\r\n \"format\",\r\n \"files\",\r\n \"password\"\r\n ],\r\n \"description\": \"Update info.\",\r\n \"x-typia-jsDocTags\": []\r\n }\r\n }\r\n }\r\n}"
|
71
|
+
},
|
67
72
|
{
|
68
73
|
"location": "/packages/api",
|
69
74
|
"file": "tsconfig.json",
|
@@ -109,10 +114,15 @@ export const TEMPLATE = [
|
|
109
114
|
"file": "MapUtil.ts",
|
110
115
|
"content": "export namespace MapUtil {\r\n export function take<Key, T>(\r\n dict: Map<Key, T>,\r\n key: Key,\r\n generator: () => T,\r\n ): T {\r\n const oldbie: T | undefined = dict.get(key);\r\n if (oldbie) return oldbie;\r\n\r\n const value: T = generator();\r\n dict.set(key, value);\r\n return value;\r\n }\r\n}\r\n"
|
111
116
|
},
|
117
|
+
{
|
118
|
+
"location": "/test/helpers",
|
119
|
+
"file": "ArgumentParser.ts",
|
120
|
+
"content": "import commander from 'commander';\r\nimport * as inquirer from 'inquirer';\r\n\r\nexport namespace ArgumentParser {\r\n export type Inquiry<T> = (\r\n command: commander.Command,\r\n prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule,\r\n action: (closure: (options: Partial<T>) => Promise<T>) => Promise<T>,\r\n ) => Promise<T>;\r\n\r\n export interface Prompt {\r\n select: (\r\n name: string,\r\n ) => (\r\n message: string,\r\n ) => <Choice extends string>(choices: Choice[]) => Promise<Choice>;\r\n boolean: (name: string) => (message: string) => Promise<boolean>;\r\n }\r\n\r\n export const parse = async <T>(\r\n inquiry: (\r\n command: commander.Command,\r\n prompt: Prompt,\r\n action: (\r\n closure: (options: Partial<T>) => Promise<T>,\r\n ) => Promise<T>,\r\n ) => Promise<T>,\r\n ): Promise<T> => {\r\n // TAKE OPTIONS\r\n const action = (closure: (options: Partial<T>) => Promise<T>) =>\r\n new Promise<T>((resolve, reject) => {\r\n commander.program.action(async (options) => {\r\n try {\r\n resolve(await closure(options));\r\n } catch (exp) {\r\n reject(exp);\r\n }\r\n });\r\n commander.program.parseAsync().catch(reject);\r\n });\r\n\r\n const select =\r\n (name: string) =>\r\n (message: string) =>\r\n async <Choice extends string>(choices: Choice[]): Promise<Choice> =>\r\n (\r\n await inquirer.createPromptModule()({\r\n type: 'list',\r\n name,\r\n message,\r\n choices,\r\n })\r\n )[name];\r\n const boolean = (name: string) => async (message: string) =>\r\n (\r\n await inquirer.createPromptModule()({\r\n type: 'confirm',\r\n name,\r\n message,\r\n })\r\n )[name] as boolean;\r\n\r\n const output: T | Error = await (async () => {\r\n try {\r\n return await inquiry(\r\n commander.program,\r\n { select, boolean },\r\n action,\r\n );\r\n } catch (error) {\r\n return error as Error;\r\n }\r\n })();\r\n\r\n // RETURNS\r\n if (output instanceof Error) throw output;\r\n return output;\r\n };\r\n}\r\n"
|
121
|
+
},
|
112
122
|
{
|
113
123
|
"location": "/test",
|
114
124
|
"file": "index.ts",
|
115
|
-
"content": "import { DynamicExecutor } from \"@nestia/e2e\";\r\n\r\nimport api from \"@ORGANIZATION/PROJECT-api\";\r\n\r\nimport { Backend } from \"../src/Backend\";\r\nimport { Configuration } from \"../src/Configuration\";\r\nimport { SGlobal } from \"../src/SGlobal\";\r\n\r\nasync function main(): Promise<void> {\r\n SGlobal.testing = true;\r\n\r\n // BACKEND SERVER\r\n const backend: Backend = new Backend();\r\n await backend.open();\r\n\r\n //----\r\n // CLINET CONNECTOR\r\n //----\r\n // DO TEST\r\n const connection: api.IConnection = {\r\n host: `http://127.0.0.1:${await Configuration.API_PORT()}`,\r\n };\r\n const report: DynamicExecutor.IReport = await DynamicExecutor.validate({\r\n prefix: \"test\",\r\n parameters: () => [connection],\r\n })(__dirname + \"/features\");\r\n\r\n await backend.close();\r\n\r\n const failures: DynamicExecutor.IReport.IExecution[] =\r\n report.executions.filter((exec) => exec.error !== null);\r\n if (failures.length === 0) {\r\n console.log(\"Success\");\r\n console.log(\"Elapsed time\", report.time.toLocaleString(), `ms`);\r\n } else {\r\n for (const f of failures) console.log(f.error);\r\n process.exit(-1);\r\n }\r\n}\r\nmain().catch((exp) => {\r\n console.log(exp);\r\n process.exit(-1);\r\n});\r\n"
|
125
|
+
"content": "import { DynamicExecutor } from \"@nestia/e2e\";\r\n\r\nimport api from \"@ORGANIZATION/PROJECT-api\";\r\n\r\nimport { Backend } from \"../src/Backend\";\r\nimport { Configuration } from \"../src/Configuration\";\r\nimport { SGlobal } from \"../src/SGlobal\";\r\nimport { ArgumentParser } from \"./helpers/ArgumentParser\";\r\n\r\ninterface IOptions {\r\n include?: string[];\r\n exclude?: string[];\r\n}\r\n\r\nconst getOptions = () =>\r\n ArgumentParser.parse<IOptions>(async (command, prompt, action) => {\r\n // command.option(\"--mode <string>\", \"target mode\");\r\n // command.option(\"--reset <true|false>\", \"reset local DB or not\");\r\n command.option(\"--include <string...>\", \"include feature files\");\r\n command.option(\"--exclude <string...>\", \"exclude feature files\");\r\n\r\n prompt;\r\n\r\n return action(async (options) => {\r\n // if (typeof options.reset === \"string\")\r\n // options.reset = options.reset === \"true\";\r\n // options.mode ??= await prompt.select(\"mode\")(\"Select mode\")([\r\n // \"LOCAL\",\r\n // \"DEV\",\r\n // \"REAL\",\r\n // ]);\r\n // options.reset ??= await prompt.boolean(\"reset\")(\"Reset local DB\");\r\n return options as IOptions;\r\n });\r\n });\r\n\r\nasync function main(): Promise<void> {\r\n const options: IOptions = await getOptions();\r\n SGlobal.testing = true;\r\n\r\n // BACKEND SERVER\r\n const backend: Backend = new Backend();\r\n await backend.open();\r\n\r\n //----\r\n // CLINET CONNECTOR\r\n //----\r\n // DO TEST\r\n const connection: api.IConnection = {\r\n host: `http://127.0.0.1:${await Configuration.API_PORT()}`,\r\n };\r\n const report: DynamicExecutor.IReport = await DynamicExecutor.validate({\r\n prefix: \"test\",\r\n parameters: () => [\r\n {\r\n host: connection.host,\r\n encryption: connection.encryption,\r\n },\r\n ],\r\n filter: (func) =>\r\n (!options.include?.length ||\r\n (options.include ?? []).some((str) => func.includes(str))) &&\r\n (!options.exclude?.length ||\r\n (options.exclude ?? []).every((str) => !func.includes(str))),\r\n })(__dirname + \"/features\");\r\n\r\n await backend.close();\r\n\r\n const failures: DynamicExecutor.IReport.IExecution[] =\r\n report.executions.filter((exec) => exec.error !== null);\r\n if (failures.length === 0) {\r\n console.log(\"Success\");\r\n console.log(\"Elapsed time\", report.time.toLocaleString(), `ms`);\r\n } else {\r\n for (const f of failures) console.log(f.error);\r\n process.exit(-1);\r\n }\r\n}\r\nmain().catch((exp) => {\r\n console.log(exp);\r\n process.exit(-1);\r\n});\r\n"
|
116
126
|
},
|
117
127
|
{
|
118
128
|
"location": "/test",
|
package/src/executable/bundle.ts
CHANGED
@@ -14,7 +14,7 @@ export namespace ControllerProgrammer {
|
|
14
14
|
for (const [path, collection] of Object.entries(swagger.paths)) {
|
15
15
|
// PREPARE DIRECTORIES
|
16
16
|
const location: string = StringUtil.split(path)
|
17
|
-
.filter((str) => str[0] !== "{")
|
17
|
+
.filter((str) => str[0] !== "{" && str[0] !== ":")
|
18
18
|
.join("/");
|
19
19
|
for (const s of sequence(location)) MapUtil.take(dict)(s)(() => []);
|
20
20
|
|
@@ -14,11 +14,11 @@ export namespace RouteProgrammer {
|
|
14
14
|
(props: { path: string; method: string }) =>
|
15
15
|
(route: ISwaggerRoute): IMigrateRoute | null => {
|
16
16
|
const body = emplaceBodySchema(emplaceReference(swagger)("body"))(
|
17
|
-
route.requestBody
|
17
|
+
route.requestBody,
|
18
18
|
);
|
19
19
|
const response = emplaceBodySchema(
|
20
20
|
emplaceReference(swagger)("response"),
|
21
|
-
)(
|
21
|
+
)(route.responses?.["201"] ?? route.responses?.["200"]);
|
22
22
|
if (body === false || response === false) {
|
23
23
|
console.log(
|
24
24
|
`Failed to migrate ${props.method.toUpperCase()} ${
|
@@ -103,7 +103,7 @@ export namespace RouteProgrammer {
|
|
103
103
|
required: [
|
104
104
|
...primitives
|
105
105
|
.filter((p) => p.required)
|
106
|
-
.map((p) => p.name),
|
106
|
+
.map((p) => p.name!),
|
107
107
|
...(dto ? dto.required ?? [] : []),
|
108
108
|
],
|
109
109
|
},
|
@@ -146,11 +146,24 @@ export namespace RouteProgrammer {
|
|
146
146
|
});
|
147
147
|
});
|
148
148
|
|
149
|
-
const parameterNames:
|
150
|
-
(
|
151
|
-
|
152
|
-
|
153
|
-
|
149
|
+
const parameterNames: string[] = StringUtil.split(props.path)
|
150
|
+
.filter((str) => str[0] === "{" || str[0] === ":")
|
151
|
+
.map((str) =>
|
152
|
+
str[0] === "{"
|
153
|
+
? str.substring(1, str.length - 1)
|
154
|
+
: str.substring(1),
|
155
|
+
);
|
156
|
+
if (
|
157
|
+
parameterNames.length !==
|
158
|
+
(route.parameters ?? []).filter((p) => p.in === "path").length
|
159
|
+
) {
|
160
|
+
console.log(
|
161
|
+
`Failed to migrate ${props.method.toUpperCase()} ${
|
162
|
+
props.path
|
163
|
+
}: number of path parameters are not matched with its full path.`,
|
164
|
+
);
|
165
|
+
return null;
|
166
|
+
}
|
154
167
|
return {
|
155
168
|
name: "@lazy",
|
156
169
|
path: props.path,
|
@@ -158,14 +171,16 @@ export namespace RouteProgrammer {
|
|
158
171
|
headers,
|
159
172
|
parameters: (route.parameters ?? [])
|
160
173
|
.filter((p) => p.in === "path")
|
161
|
-
.map((p) => ({
|
174
|
+
.map((p, i) => ({
|
162
175
|
key: (() => {
|
163
|
-
let key: string = StringUtil.normalize(
|
176
|
+
let key: string = StringUtil.normalize(
|
177
|
+
parameterNames[i],
|
178
|
+
);
|
164
179
|
if (Escaper.variable(key)) return key;
|
165
180
|
|
166
181
|
while (true) {
|
167
182
|
key = "_" + key;
|
168
|
-
if (parameterNames.
|
183
|
+
if (!parameterNames.some((s) => s === key))
|
169
184
|
return key;
|
170
185
|
}
|
171
186
|
})(),
|
@@ -178,32 +193,47 @@ export namespace RouteProgrammer {
|
|
178
193
|
body,
|
179
194
|
response,
|
180
195
|
description: describe(route),
|
196
|
+
"x-nestia-jsDocTags": route["x-nestia-jsDocTags"],
|
181
197
|
};
|
182
198
|
};
|
183
199
|
|
184
200
|
const describe = (route: ISwaggerRoute): string | undefined => {
|
185
|
-
const
|
201
|
+
const commentTags: string[] = [];
|
186
202
|
const add = (text: string) => {
|
187
|
-
if (
|
188
|
-
|
203
|
+
if (commentTags.every((line) => line !== text))
|
204
|
+
commentTags.push(text);
|
189
205
|
};
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
206
|
+
|
207
|
+
let description: string | undefined = route.description;
|
208
|
+
if (route.summary) {
|
209
|
+
const emended: string = route.summary.endsWith(".")
|
210
|
+
? route.summary
|
211
|
+
: route.summary + ".";
|
212
|
+
if (
|
213
|
+
description !== undefined &&
|
214
|
+
!description?.startsWith(route.summary) &&
|
215
|
+
!route["x-nestia-jsDocTags"]?.some((t) => t.name === "summary")
|
216
|
+
)
|
217
|
+
description = `${emended}\n${description}`;
|
200
218
|
}
|
201
219
|
if (route.tags) route.tags.forEach((name) => add(`@tag ${name}`));
|
202
220
|
if (route.deprecated) add("@deprecated");
|
203
221
|
for (const security of route.security ?? [])
|
204
222
|
for (const [name, scopes] of Object.entries(security))
|
205
223
|
add(`@security ${[name, ...scopes].join("")}`);
|
206
|
-
|
224
|
+
for (const jsDocTag of route["x-nestia-jsDocTags"] ?? [])
|
225
|
+
if (jsDocTag.text?.length)
|
226
|
+
add(
|
227
|
+
`@${jsDocTag.name} ${jsDocTag.text
|
228
|
+
.map((text) => text.text)
|
229
|
+
.join("")}`,
|
230
|
+
);
|
231
|
+
else add(`@${jsDocTag.name}`);
|
232
|
+
return description?.length
|
233
|
+
? commentTags.length
|
234
|
+
? `${description}\n\n${commentTags.join("\n")}`
|
235
|
+
: description
|
236
|
+
: commentTags.join("\n");
|
207
237
|
};
|
208
238
|
|
209
239
|
const isNotObjectLiteral = (schema: ISwaggerSchema): boolean =>
|
@@ -220,14 +250,21 @@ export namespace RouteProgrammer {
|
|
220
250
|
|
221
251
|
const emplaceBodySchema =
|
222
252
|
(emplacer: (schema: ISwaggerSchema) => ISwaggerSchema.IReference) =>
|
223
|
-
(
|
224
|
-
|
225
|
-
|
226
|
-
|
253
|
+
(meta?: {
|
254
|
+
description?: string;
|
255
|
+
content?: ISwaggerRoute.IContent;
|
256
|
+
"x-nestia-encrypted"?: boolean;
|
257
|
+
}): false | null | IMigrateRoute.IBody => {
|
258
|
+
if (!meta?.content) return null;
|
227
259
|
|
228
260
|
const entries: [string, { schema: ISwaggerSchema }][] =
|
229
|
-
Object.entries(content);
|
230
|
-
const json = entries.find((e) =>
|
261
|
+
Object.entries(meta.content);
|
262
|
+
const json = entries.find((e) =>
|
263
|
+
meta["x-nestia-encrypted"] === true
|
264
|
+
? e[0].includes("text/plain") ||
|
265
|
+
e[0].includes("application/json")
|
266
|
+
: e[0].includes("application/json"),
|
267
|
+
);
|
231
268
|
|
232
269
|
if (json) {
|
233
270
|
const { schema } = json[1];
|
@@ -236,6 +273,7 @@ export namespace RouteProgrammer {
|
|
236
273
|
schema: isNotObjectLiteral(schema)
|
237
274
|
? schema
|
238
275
|
: emplacer(schema),
|
276
|
+
"x-nestia-encrypted": meta["x-nestia-encrypted"],
|
239
277
|
};
|
240
278
|
}
|
241
279
|
|
@@ -260,7 +298,9 @@ export namespace RouteProgrammer {
|
|
260
298
|
? SchemaProgrammer.write(references)(route.response.schema)
|
261
299
|
: "void";
|
262
300
|
const decorator: string =
|
263
|
-
route.body?.
|
301
|
+
route.body?.["x-nestia-encrypted"] === true
|
302
|
+
? "@core.EncryptedRoute."
|
303
|
+
: route.body?.type === "text/plain"
|
264
304
|
? [`@Header("Content-Type", "text/plain")`, `@`].join("\n")
|
265
305
|
: "@core.TypedRoute.";
|
266
306
|
const content: string[] = [
|
@@ -278,15 +318,28 @@ export namespace RouteProgrammer {
|
|
278
318
|
}`,
|
279
319
|
`public async ${route.name}(`,
|
280
320
|
...route.parameters.map((p) => ` ${writeParameter(p)},`),
|
321
|
+
...(route.headers
|
322
|
+
? [
|
323
|
+
` @core.TypedHeaders() headers: ${SchemaProgrammer.write(
|
324
|
+
references,
|
325
|
+
)(route.headers)},`,
|
326
|
+
]
|
327
|
+
: []),
|
281
328
|
...(route.query
|
282
329
|
? [
|
283
330
|
` @core.TypedQuery() query: ${SchemaProgrammer.write(
|
284
331
|
references,
|
285
|
-
)(route.query)}
|
332
|
+
)(route.query)},`,
|
286
333
|
]
|
287
334
|
: []),
|
288
335
|
...(route.body
|
289
|
-
? route.body
|
336
|
+
? route.body["x-nestia-encrypted"] === true
|
337
|
+
? [
|
338
|
+
` @core.EncryptedBody() body: ${SchemaProgrammer.write(
|
339
|
+
references,
|
340
|
+
)(route.body.schema)},`,
|
341
|
+
]
|
342
|
+
: route.body.type === "application/json"
|
290
343
|
? [
|
291
344
|
` @core.TypedBody() body: ${SchemaProgrammer.write(
|
292
345
|
references,
|
@@ -298,7 +351,7 @@ export namespace RouteProgrammer {
|
|
298
351
|
...route.parameters.map(
|
299
352
|
(p) => ` ${StringUtil.normalize(p.key)};`,
|
300
353
|
),
|
301
|
-
|
354
|
+
...(route.headers ? [" headers;"] : []),
|
302
355
|
...(route.query ? [" query;"] : []),
|
303
356
|
...(route.body ? [" body;"] : []),
|
304
357
|
...(output !== "void"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { IJsDocTagInfo } from "typia/lib/metadata/IJsDocTagInfo";
|
2
|
+
|
1
3
|
import { ISwaggerSchema } from "./ISwaggeSchema";
|
2
4
|
|
3
5
|
export interface IMigrateRoute {
|
@@ -10,6 +12,7 @@ export interface IMigrateRoute {
|
|
10
12
|
body: IMigrateRoute.IBody | null;
|
11
13
|
response: IMigrateRoute.IBody | null;
|
12
14
|
description?: string;
|
15
|
+
"x-nestia-jsDocTags"?: IJsDocTagInfo[];
|
13
16
|
}
|
14
17
|
export namespace IMigrateRoute {
|
15
18
|
export interface IParameter {
|
@@ -20,5 +23,6 @@ export namespace IMigrateRoute {
|
|
20
23
|
export interface IBody {
|
21
24
|
type: "text/plain" | "application/json";
|
22
25
|
schema: ISwaggerSchema;
|
26
|
+
"x-nestia-encrypted"?: boolean;
|
23
27
|
}
|
24
28
|
}
|