diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a492d49..ab324cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,13 +6,14 @@ on: - 'test/**' - 'package.json' - 'tsconfig.json' + - .github/workflows/build.yml pull_request: paths: - 'src/**' - 'test/**' - 'package.json' - 'tsconfig.json' - + - .github/workflows/build.yml jobs: Ubuntu: runs-on: ubuntu-latest @@ -21,12 +22,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20.x - - uses: pnpm/action-setup@v2 - with: - version: 8 - - name: Install dependencies - run: pnpm install + run: npm install - name: Build run: npm run build - name: Test diff --git a/README.md b/README.md index 4919720..976ae09 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ main().catch(console.error); ## Features About supported features, please read description comments of each component. -I'm preparing documentation and playground website of `@wrtnio/openai-function-schema` features. Until that, please read below components' description comments. Even though you have to read source code of each component, but description comments would satisfy you. +I'm preparing documentation and playground website of `@wrtnio/openai-function-schema` features. Until that, please read below components' description comments. Even though you have to read source code of each component, but description comments of them may satisfy you. - Schema Definitions - [`IOpenAiDocument`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts): OpenAI function metadata collection with options @@ -76,3 +76,234 @@ I'm preparing documentation and playground website of `@wrtnio/openai-function-s - [`OpenAiFetcher`](https://github.com/wrtnio/openai-function-schema/blob/master/src/OpenAiFetcher.ts): Function call executor with `IOpenAiFunction` - [`OpenAiDataCombiner`](https://github.com/wrtnio/openai-function-schema/blob/master/src/OpenAiDataCombiner.ts): Data combiner for LLM function call with human composed data - [`OpenAiTypeChecker`](https://github.com/wrtnio/openai-function-schema/blob/master/src/OpenAiTypeChecker.ts): Type checker for `IOpenAiSchema` + +### Command Line Interface +```bash +######## +# LAUNCH CLI +######## +# PRIOR TO NODE V20 +npm install -g @wrtnio/openai-function-schema +npx wofs + +# SINCE NODE V20 +npx @wrtnio/openai-function-schema + +######## +# PROMPT +######## +-------------------------------------------------------- + Swagger to OpenAI Function Call Schema Converter +-------------------------------------------------------- +? Swagger file path: test/swagger.json +? OpenAI Function Call Schema file path: test/plain.json +? Whether to wrap parameters into an object with keyword or not: No +``` + +Convert swagger to OpenAI function schema file by a CLI command. + +If you run `npx @wrtnio/openai-function-schema` (or `npx wofs` after global setup), the CLI (Command Line Interface) will inquiry those arguments. After you fill all of them, the OpenAI fuction call schema file of [`IOpenAiDocument`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts) type would be created to the target location. + +If you want to specify arguments without prompting, you can fill them like below: + +```bash +# PRIOR TO NODE V20 +npm install -g @wrtnio/openai-function-schema +npx wofs --input swagger.json --output openai.json --keyword false + +# SINCE NODE V20 +npx @wrtnio/openai-function-schema + --input swagger.json + --output openai.json + --keyword false +``` + + + + +### Library API +If you want to utilize `@wrtnio/openai-function-schema` in the API level, you should start from composing [`IOpenAiDocument`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts) through `OpenAiComposer.document()` method. + +After composing the [`IOpenAiDocument`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts) data, you may provide the nested [`IOpenAiFunction`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiFunction.ts) instances to the OpenAI, and the OpenAI may compose the arguments by its function calling feature. With the OpenAI automatically composed arguments, you can execute the function call by `OpenAiFetcher.execute()` method. + +Here is the example code composing and executing the [`IOpenAiFunction`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiFunction.ts). + + - Test Function: [test_fetcher_positional_bbs_article_update.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/features/fetcher/positional/test_fetcher_positional_bbs_article_update.ts) + - Backend Server Code: [BbsArticlesController.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/controllers/BbsArticlesController.ts) + +```typescript +import { + IOpenAiDocument, + IOpenAiFunction, + OpenAiComposer, + OpenAiFetcher, +} from "@wrtnio/openai-function-schema"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { IBbsArticle } from "../../../api/structures/IBbsArticle"; + +const main = async (): Promise => { + // COMPOSE OPENAI FUNCTION CALL SCHEMAS + const swagger = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + const document: IOpenAiDocument = OpenAiComposer.document({ + swagger + }); + + // EXECUTE OPENAI FUNCTION CALL + const func: IOpenAiFunction = document.functions.find( + (f) => f.method === "put" && f.path === "/bbs/articles", + )!; + const article: IBbsArticle = await OpenAiFetcher.execute({ + document, + function: func, + connection: { host: "http://localhost:3000" }, + arguments: [ + // imagine that arguments are composed by OpenAI + v4(), + typia.random(), + ], + }); + typia.assert(article); +}; +main().catch(console.error); +``` + +By the way, above example code's target operation function has multiple parameters. You know what? If you configure a function to have only one parameter by wrapping into one object type, OpenAI function calling feature constructs arguments a little bit efficiently than multiple parameters case. + +Such only one object typed parameter is called `keyword parameter`, and `@wrtnio/openai-function-schema` supports such keyword parameterized function schemas. When composing [`IOpenAiDocument`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts) by `OpenAiComposer.document()` method, configures `option.keyword` to be `true`, then every [`IOpenAiFunction`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiFunction.ts) instances would be keyword parameterized. Also, `OpenAiFetcher` understands the keyword parameterized function specification, so that performs proper execution by automatic decomposing the arguments. + +Here is the example code of keyword parameterizing. + + - Test Function: [test_fetcher_keyword_bbs_article_update.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/features/fetcher/keyword/test_fetcher_keyword_bbs_article_update.ts) + - Backend Server Code: [BbsArticlesController.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/controllers/BbsArticlesController.ts) + +```typescript +import { + IOpenAiDocument, + IOpenAiFunction, + OpenAiComposer, + OpenAiFetcher, +} from "@wrtnio/openai-function-schema"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { IBbsArticle } from "../../../api/structures/IBbsArticle"; + +const main = async (): Promise => { + // COMPOSE OPENAI FUNCTION CALL SCHEMAS + const swagger = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + const document: IOpenAiDocument = OpenAiComposer.document({ + swagger, + options: { + keyword: true, // keyword parameterizing + } + }); + + // EXECUTE OPENAI FUNCTION CALL + const func: IOpenAiFunction = document.functions.find( + (f) => f.method === "put" && f.path === "/bbs/articles", + )!; + const article: IBbsArticle = await OpenAiFetcher.execute({ + document, + function: func, + connection: { host: "http://localhost:3000" }, + arguments: [ + // imagine that argument is composed by OpenAI + { + id: v4(), + body: typia.random(), + }, + ], + }); + typia.assert(article); +}; +main().catch(console.error); +``` + +At last, there can be some special API operation that some arguments must be composed by user, not by LLM (Large Language Model). For example, if an API operation requires file uploading or secret key identifier, it must be composed by user manually in the frontend application side. + +For such case, `@wrtnio/openai-function-schema` supports special option [`IOpenAiDocument.IOptions.separate`](https://github.com/wrtnio/openai-function-schema/blob/master/src/structures/IOpenAiDocument.ts). If you configure the callback function, it would be utilized for determining whether the value must be composed by user or not. When the arguments are composed by both user and LLM sides, you can combine them into one through `OpenAiDataComposer.parameters()` method, so that you can still execute the function calling with `OpenAiFetcher.execute()` method. + +Here is the example code of such special case: + + - Test Function: [test_combiner_keyword_parameters_query.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/features/combiner/test_combiner_keyword_parameters_query.ts) + - Backend Server Code: [MembershipController.ts](https://github.com/wrtnio/openai-function-schema/blob/main/test/controllers/MembershipController.ts) + +```typescript +import { + IOpenAiDocument, + IOpenAiFunction, + IOpenAiSchema, + OpenAiComposer, + OpenAiDataCombiner, + OpenAiFetcher, + OpenAiTypeChecker, +} from "@wrtnio/openai-function-schema"; +import fs from "fs"; +import typia from "typia"; + +import { IMembership } from "../../api/structures/IMembership"; + +const main = async (): Promise => { + // COMPOSE OPENAI FUNCTION CALL SCHEMAS + const swagger = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + const document: IOpenAiDocument = OpenAiComposer.document({ + swagger, + options: { + keyword: true, + separate: (schema: IOpenAiSchema) => + OpenAiTypeChecker.isString(schema) && + (schema["x-wrtn-secret-key"] !== undefined || + schema["contentMediaType"] !== undefined), + }, + }); + + // EXECUTE OPENAI FUNCTION CALL + const func: IOpenAiFunction = document.functions.find( + (f) => f.method === "patch" && f.path === "/membership/change", + )!; + const membership: IMembership = await OpenAiFetcher.execute({ + document, + function: func, + connection: { host: "http://localhost:3000" }, + arguments: OpenAiDataCombiner.parameters({ + function: func, + llm: [ + // imagine that below argument is composed by OpenAI + { + body: { + name: "Wrtn Technologies", + email: "master@wrtn.io", + password: "1234", + age: 20, + gender: 1, + }, + }, + ], + human: [ + // imagine that below argument is composed by human + { + query: { + secret: "something", + }, + body: { + secretKey: "something", + picture: "https://wrtn.io/logo.png", + }, + }, + ], + }), + }); + typia.assert(membership); +}; +main().catch(console.error); +``` diff --git a/package.json b/package.json index 2727379..66b89cb 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,22 @@ { "name": "@wrtnio/openai-function-schema", - "version": "0.1.0", + "version": "0.1.1", "description": "OpenAI LLM function schema from OpenAPI (Swagger) document", "main": "lib/index.js", "typings": "lib/index.d.ts", "module": "lib/index.mjs", + "bin": { + "wofs": "lib/executable/wofs.js" + }, "scripts": { "prepare": "ts-patch install", "build": "npm run build:main && npm run build:test", "build:main": "rimraf lib && tsc && rollup -c", "build:test": "rimraf bin && tsc -p test/tsconfig.json", "dev": "npm run build:test -- --watch", - "test": "node bin/test" + "test": "npm run test:api && npm run test:cli", + "test:api": "node bin/test", + "test:cli": "node bin/src/executable/wofs.js --input test/swagger.json --output test/plain.json --keyword false" }, "keywords": [ "openai", @@ -30,7 +35,10 @@ "license": "ISC", "dependencies": { "@nestia/fetcher": "^3.4.1", - "@samchon/openapi": "^0.3.0" + "@samchon/openapi": "^0.3.0", + "commander": "^10.0.0", + "inquirer": "^8.2.5", + "typia": "^6.4.0" }, "devDependencies": { "@nestia/core": "^3.4.1", @@ -42,6 +50,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/inquirer": "^8.2.5", "@types/node": "^20.14.9", "@types/uuid": "^10.0.0", "nestia": "^5.3.1", @@ -51,7 +60,6 @@ "ts-patch": "^3.2.1", "typescript": "5.5.2", "typescript-transform-paths": "^3.4.7", - "typia": "^6.4.0", "uuid": "^10.0.0" }, "files": [ diff --git a/src/OpenAiComposer.ts b/src/OpenAiComposer.ts index 0b32288..67f85ec 100644 --- a/src/OpenAiComposer.ts +++ b/src/OpenAiComposer.ts @@ -8,6 +8,7 @@ import { } from "@samchon/openapi"; import { OpenApiTypeChecker } from "@samchon/openapi/lib/internal/OpenApiTypeChecker"; import { OpenApiV3Downgrader } from "@samchon/openapi/lib/internal/OpenApiV3Downgrader"; +import typia from "typia"; import { OpenAiSchemaSeparator } from "./internal/OpenAiSchemaSeparator"; import { IOpenAiSchema, ISwaggerOperation } from "./module"; @@ -92,7 +93,8 @@ export namespace OpenAiComposer { */ export const document = (props: IProps): IOpenAiDocument => { // LIST UP ARGUMENTS - const swagger: ISwagger = OpenApi.convert(props.swagger) as any; + typia.assert(props); + const swagger: ISwagger = OpenApi.convert(props.swagger); const options: IOpenAiDocument.IOptions = { keyword: props.options?.keyword ?? false, separate: props.options?.separate ?? null, diff --git a/src/executable/internal/ArgumentParser.ts b/src/executable/internal/ArgumentParser.ts new file mode 100644 index 0000000..bc18e3a --- /dev/null +++ b/src/executable/internal/ArgumentParser.ts @@ -0,0 +1,97 @@ +import commander from "commander"; +import * as inquirer from "inquirer"; + +export namespace ArgumentParser { + export type Inquiry = ( + command: commander.Command, + prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise; + + export interface Prompt { + input: (name: string) => (message: string) => Promise; + select: ( + name: string, + ) => ( + message: string, + ) => (choices: Choice[]) => Promise; + boolean: (name: string) => (message: string) => Promise; + number: (name: string) => (message: string) => Promise; + } + + export const parse = async ( + inquiry: ( + command: commander.Command, + prompt: Prompt, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise, + ): Promise => { + // TAKE OPTIONS + const action = (closure: (options: Partial) => Promise) => + new Promise((resolve, reject) => { + commander.program.action(async (options) => { + try { + resolve(await closure(options)); + } catch (exp) { + reject(exp); + } + }); + commander.program.parseAsync().catch(reject); + }); + + const input = (name: string) => async (message: string) => + ( + await inquirer.createPromptModule()({ + type: "input", + name, + message, + }) + )[name]; + const select = + (name: string) => + (message: string) => + async (choices: Choice[]): Promise => + ( + await inquirer.createPromptModule()({ + type: "list", + name, + message, + choices, + }) + )[name]; + const boolean = (name: string) => async (message: string) => + ( + await inquirer.createPromptModule()({ + type: "confirm", + name, + message, + }) + )[name] as boolean; + const number = (name: string) => async (message: string) => + Number( + ( + await inquirer.createPromptModule()({ + type: "number", + name, + message, + }) + )[name], + ); + + const output: T | Error = await (async () => { + try { + return await inquiry( + commander.program, + { select, boolean, number, input }, + action, + ); + } catch (error) { + return error as Error; + } + })(); + + // RETURNS + if (output instanceof Error) throw output; + return output; + }; +} diff --git a/src/executable/wofs.ts b/src/executable/wofs.ts new file mode 100644 index 0000000..2826a74 --- /dev/null +++ b/src/executable/wofs.ts @@ -0,0 +1,62 @@ +#!/usr/bin/env node +import { OpenApi, OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@samchon/openapi"; +import fs from "fs"; + +import { OpenAiComposer } from "../OpenAiComposer"; +import { ArgumentParser } from "./internal/ArgumentParser"; + +interface IOptions { + input: string; + output: string; + keyword: boolean; +} +const main = async (): Promise => { + console.log("--------------------------------------------------------"); + console.log(" Swagger to OpenAI Function Call Schema Converter"); + console.log("--------------------------------------------------------"); + + const options: IOptions = await ArgumentParser.parse( + async (command, prompt, action) => { + command.option("--input ", "Swagger file path"); + command.option( + "--output ", + "OpenAI Function Call Schema file path", + ); + command.option( + "--keyword ", + "Whether to wrap parameters into an object with keyword or not", + ); + return action(async (options) => { + options.input ??= await prompt.input("input")("Swagger file path:"); + options.output ??= await prompt.input("output")( + "OpenAI Function Call Schema file path:", + ); + if (typeof options.keyword === "string") + options.keyword = + options.keyword === "true" || options.keyword === ""; + options.keyword ??= await prompt.boolean("keyword")( + "Whether to wrap parameters into an object with keyword or not:", + ); + return options as IOptions; + }); + }, + ); + const swagger: + | OpenApi.IDocument + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile(options.input, "utf-8"), + ); + const document = OpenAiComposer.document({ + swagger, + options: { + keyword: options.keyword, + }, + }); + await fs.promises.writeFile( + options.output, + JSON.stringify(document, null, 2), + ); +}; +main().catch(console.error); diff --git a/src/index.ts b/src/index.ts index 771ca05..2c2a2cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import * as wfc from "./module"; +import * as wofs from "./module"; export * from "./module"; -export default wfc; +export default wofs; diff --git a/test/plain.json b/test/plain.json new file mode 100644 index 0000000..4de3b76 --- /dev/null +++ b/test/plain.json @@ -0,0 +1,1619 @@ +{ + "openapi": "3.0.3", + "functions": [ + { + "method": "post", + "path": "/bbs/articles", + "name": "bbs_articles_post", + "parameters": [ + { + "type": "object", + "properties": { + "writer": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ], + "title": "Format of body", + "description": "Format of body.\n\nSame meaning with extension like `html`, `md`, `txt`." + }, + "title": { + "type": "string", + "title": "Title of article", + "description": "Title of article." + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + }, + "password": { + "type": "string", + "title": "Password for modification", + "description": "Password for modification." + } + }, + "required": [ + "writer", + "format", + "title", + "body", + "files", + "password" + ], + "description": "Store content type of the article." + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "writer": { + "type": "string", + "title": "Writer of article", + "description": "Writer of article." + }, + "snapshots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of snapshot record", + "description": "Creation time of snapshot record.\n\nIn other words, creation time or update time or article." + }, + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ] + }, + "title": { + "type": "string", + "title": "Title of article", + "description": "Title of article." + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + } + }, + "required": [ + "id", + "created_at", + "format", + "title", + "body", + "files" + ], + "description": "Snapshot of article.\n\n`IBbsArticle.ISnapshot` is a snapshot entity that contains the contents of\nthe article, as mentioned in {@link IBbsArticle}, the contents of the article\nare separated from the article record to keep evidence and prevent fraud." + }, + "minItems": 1, + "title": "List of snapshot contents", + "description": "List of snapshot contents.\n\nIt is created for the first time when an article is created, and is\naccumulated every time the article is modified." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of article", + "description": "Creation time of article." + } + }, + "required": [ + "id", + "writer", + "snapshots", + "created_at" + ], + "description": "Article entity.\n\n`IBbsArticle* is a super-type entity of all kinds of articles in the current\nbackend system, literally shaping individual articles of the bulletin board.\n\nAnd, as you can see, the elements that must inevitably exist in the article,\nsuch as the `title` or the `body`, do not exist in the `IBbsArticle`, but exist\nin the subsidiary entity, {@link IBbsArticle.ISnapshot}, as a 1: N relationship,\nwhich is because a new snapshot record is published every time the article is\nmodified.\n\nThe reason why a new snapshot record is published every time the article is\nmodified is to preserve the evidence. Due to the nature of e-community, there\nis always a threat of dispute among the participants. And it can happen that\ndisputes arise through articles or {@link IBbsArticleComment comments}, and to\nprevent such things as modifying existing articles to manipulate the situation,\nthe article is designed in this structure.\n\nIn other words, to keep evidence, and prevent fraud." + }, + "description": "Create a new article.\n\nCreate a new article with its first {@link IBbsArticle.ISnapshot snapshot}." + }, + { + "method": "patch", + "path": "/bbs/articles", + "name": "bbs_articles_patch", + "parameters": [ + { + "type": "object", + "properties": { + "page": { + "type": "integer", + "title": "Page number", + "description": "Page number." + }, + "limit": { + "type": "integer", + "title": "Limitation of records per a page", + "description": "Limitation of records per a page." + } + }, + "description": "Page request data" + } + ], + "output": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "current": { + "type": "integer", + "title": "Current page number", + "description": "Current page number." + }, + "limit": { + "type": "integer", + "title": "Limitation of records per a page", + "description": "Limitation of records per a page." + }, + "records": { + "type": "integer", + "title": "Total records in the database", + "description": "Total records in the database." + }, + "pages": { + "type": "integer", + "title": "Total pages", + "description": "Total pages.\n\nEqual to {@link records} / {@link limit} with ceiling." + } + }, + "required": [ + "current", + "limit", + "records", + "pages" + ], + "description": "Page information." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "writer": { + "type": "string", + "title": "Writer of the article", + "description": "Writer of the article." + }, + "title": { + "type": "string", + "title": "Title of the last snapshot", + "description": "Title of the last snapshot." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of the article", + "description": "Creation time of the article." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Modification time of the article", + "description": "Modification time of the article.\n\nIn other words, the time when the last snapshot was created." + } + }, + "required": [ + "id", + "writer", + "title", + "created_at", + "updated_at" + ], + "description": "Summarized information of the article." + }, + "title": "List of records", + "description": "List of records." + } + }, + "required": [ + "pagination", + "data" + ], + "description": "A page.\n\nCollection of records with pagination indformation." + }, + "description": "List up all summarized articles.\n\nList up all summarized articles with pagination and searching options." + }, + { + "method": "patch", + "path": "/bbs/articles/abridges", + "name": "bbs_articles_abridges_patch", + "parameters": [ + { + "type": "object", + "properties": { + "search": { + "type": "object", + "properties": { + "writer": { + "type": "string" + }, + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "title_or_body": { + "type": "string" + }, + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + } + }, + "description": "검색 정보." + }, + "sort": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "-writer", + "-title", + "-created_at", + "-updated_at", + "+writer", + "+title", + "+created_at", + "+updated_at" + ] + }, + "title": "Sort condition", + "description": "Sort condition." + }, + "page": { + "type": "integer", + "title": "Page number", + "description": "Page number." + }, + "limit": { + "type": "integer", + "title": "Limitation of records per a page", + "description": "Limitation of records per a page." + } + } + } + ], + "output": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "current": { + "type": "integer", + "title": "Current page number", + "description": "Current page number." + }, + "limit": { + "type": "integer", + "title": "Limitation of records per a page", + "description": "Limitation of records per a page." + }, + "records": { + "type": "integer", + "title": "Total records in the database", + "description": "Total records in the database." + }, + "pages": { + "type": "integer", + "title": "Total pages", + "description": "Total pages.\n\nEqual to {@link records} / {@link limit} with ceiling." + } + }, + "required": [ + "current", + "limit", + "records", + "pages" + ], + "description": "Page information." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "writer": { + "type": "string", + "title": "Writer of the article", + "description": "Writer of the article." + }, + "title": { + "type": "string", + "title": "Title of the last snapshot", + "description": "Title of the last snapshot." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of the article", + "description": "Creation time of the article." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Modification time of the article", + "description": "Modification time of the article.\n\nIn other words, the time when the last snapshot was created." + }, + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ] + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + } + }, + "required": [ + "id", + "writer", + "title", + "created_at", + "updated_at", + "format", + "body", + "files" + ], + "description": "Abriged information of the article." + }, + "title": "List of records", + "description": "List of records." + } + }, + "required": [ + "pagination", + "data" + ], + "description": "A page.\n\nCollection of records with pagination indformation." + }, + "description": "List up all abridged articles.\n\nList up all abridged articles with pagination and searching options." + }, + { + "method": "get", + "path": "/bbs/articles/{id}", + "name": "bbs_articles_getById", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Target article's " + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "writer": { + "type": "string", + "title": "Writer of article", + "description": "Writer of article." + }, + "snapshots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of snapshot record", + "description": "Creation time of snapshot record.\n\nIn other words, creation time or update time or article." + }, + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ] + }, + "title": { + "type": "string", + "title": "Title of article", + "description": "Title of article." + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + } + }, + "required": [ + "id", + "created_at", + "format", + "title", + "body", + "files" + ], + "description": "Snapshot of article.\n\n`IBbsArticle.ISnapshot` is a snapshot entity that contains the contents of\nthe article, as mentioned in {@link IBbsArticle}, the contents of the article\nare separated from the article record to keep evidence and prevent fraud." + }, + "minItems": 1, + "title": "List of snapshot contents", + "description": "List of snapshot contents.\n\nIt is created for the first time when an article is created, and is\naccumulated every time the article is modified." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of article", + "description": "Creation time of article." + } + }, + "required": [ + "id", + "writer", + "snapshots", + "created_at" + ], + "description": "Article entity.\n\n`IBbsArticle* is a super-type entity of all kinds of articles in the current\nbackend system, literally shaping individual articles of the bulletin board.\n\nAnd, as you can see, the elements that must inevitably exist in the article,\nsuch as the `title` or the `body`, do not exist in the `IBbsArticle`, but exist\nin the subsidiary entity, {@link IBbsArticle.ISnapshot}, as a 1: N relationship,\nwhich is because a new snapshot record is published every time the article is\nmodified.\n\nThe reason why a new snapshot record is published every time the article is\nmodified is to preserve the evidence. Due to the nature of e-community, there\nis always a threat of dispute among the participants. And it can happen that\ndisputes arise through articles or {@link IBbsArticleComment comments}, and to\nprevent such things as modifying existing articles to manipulate the situation,\nthe article is designed in this structure.\n\nIn other words, to keep evidence, and prevent fraud." + }, + "description": "Read individual article.\n\nReads an article with its every {@link IBbsArticle.ISnapshot snapshots}." + }, + { + "method": "put", + "path": "/bbs/articles/{id}", + "name": "bbs_articles_putById", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Target article's " + }, + { + "type": "object", + "properties": { + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ], + "title": "Format of body", + "description": "Format of body.\n\nSame meaning with extension like `html`, `md`, `txt`." + }, + "title": { + "type": "string", + "title": "Title of article", + "description": "Title of article." + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + }, + "password": { + "type": "string", + "title": "Password for modification", + "description": "Password for modification." + } + }, + "required": [ + "format", + "title", + "body", + "files", + "password" + ] + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Primary Key", + "description": "Primary Key." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Creation time of snapshot record", + "description": "Creation time of snapshot record.\n\nIn other words, creation time or update time or article." + }, + "format": { + "type": "string", + "enum": [ + "txt", + "md", + "html" + ] + }, + "title": { + "type": "string", + "title": "Title of article", + "description": "Title of article." + }, + "body": { + "type": "string", + "title": "Content body of article", + "description": "Content body of article." + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "title": "File name, except extension", + "description": "File name, except extension." + }, + "extension": { + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true, + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case." + }, + "url": { + "type": "string", + "format": "uri", + "title": "URL path of the real file", + "description": "URL path of the real file." + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "title": "List of attachment files", + "description": "List of attachment files." + } + }, + "required": [ + "id", + "created_at", + "format", + "title", + "body", + "files" + ], + "description": "Snapshot of article.\n\n`IBbsArticle.ISnapshot` is a snapshot entity that contains the contents of\nthe article, as mentioned in {@link IBbsArticle}, the contents of the article\nare separated from the article record to keep evidence and prevent fraud." + }, + "description": "Update an article.\n\nAccumulate a new {@link IBbsArticle.ISnapshot snapshot} record to the article." + }, + { + "method": "delete", + "path": "/bbs/articles/{id}", + "name": "bbs_articles_eraseById", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Target article's " + }, + { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "required": [ + "password" + ] + } + ], + "description": "Erase an article.\n\nPerforms soft deletion to the article." + }, + { + "method": "post", + "path": "/membership/join", + "name": "membership_join_post", + "parameters": [ + { + "type": "object", + "properties": { + "secretKey": { + "type": "string", + "x-wrtn-secret-key": "wrtn" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "password": { + "type": "string" + }, + "picture": { + "type": "string", + "format": "uri", + "contentMediaType": "image/png" + } + }, + "required": [ + "name", + "email", + "age", + "gender", + "password", + "picture" + ] + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "sex": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "picture": { + "type": "string", + "contentMediaType": "image/png" + } + }, + "required": [ + "id", + "name", + "email", + "age", + "sex", + "picture" + ] + } + }, + { + "method": "patch", + "path": "/membership/login", + "name": "membership_login_patch", + "parameters": [ + { + "type": "object", + "properties": { + "secretKey": { + "type": "string", + "x-wrtn-secret-key": "wrtn" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + } + }, + "required": [ + "email", + "password" + ] + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "sex": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "picture": { + "type": "string", + "contentMediaType": "image/png" + } + }, + "required": [ + "id", + "name", + "email", + "age", + "sex", + "picture" + ] + } + }, + { + "method": "get", + "path": "/membership/{secret}", + "name": "membership_getBySecret", + "parameters": [ + { + "type": "string", + "x-wrtn-secret-key": "wrtn", + "description": "" + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "sex": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "picture": { + "type": "string", + "contentMediaType": "image/png" + } + }, + "required": [ + "id", + "name", + "email", + "age", + "sex", + "picture" + ] + } + }, + { + "method": "patch", + "path": "/membership/change", + "name": "membership_change_patch", + "parameters": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "x-wrtn-secret-key": "wrtn", + "description": "" + } + }, + "required": [ + "secret" + ] + }, + { + "type": "object", + "properties": { + "secretKey": { + "type": "string", + "x-wrtn-secret-key": "wrtn" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "password": { + "type": "string" + }, + "picture": { + "type": "string", + "format": "uri", + "contentMediaType": "image/png" + } + }, + "required": [ + "name", + "email", + "age", + "gender", + "password", + "picture" + ] + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "sex": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "picture": { + "type": "string", + "contentMediaType": "image/png" + } + }, + "required": [ + "id", + "name", + "email", + "age", + "sex", + "picture" + ] + } + }, + { + "method": "patch", + "path": "/membership/{secret}/change", + "name": "membership_change_patchBySecret", + "parameters": [ + { + "type": "string", + "x-wrtn-secret-key": "wrtn", + "description": "" + }, + { + "type": "object", + "properties": { + "secretKey": { + "type": "string", + "x-wrtn-secret-key": "wrtn" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "password": { + "type": "string" + }, + "picture": { + "type": "string", + "format": "uri", + "contentMediaType": "image/png" + } + }, + "required": [ + "name", + "email", + "age", + "gender", + "password", + "picture" + ] + } + ], + "output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer" + }, + "sex": { + "type": "number", + "enum": [ + 0, + 1, + 2 + ] + }, + "picture": { + "type": "string", + "contentMediaType": "image/png" + } + }, + "required": [ + "id", + "name", + "email", + "age", + "sex", + "picture" + ] + } + }, + { + "method": "post", + "path": "/multipart", + "name": "multipart_post", + "parameters": [ + { + "type": "object", + "properties": { + "blob": { + "type": "string", + "format": "binary" + }, + "blobs": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + }, + "file": { + "type": "string", + "format": "binary" + }, + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "flags": { + "type": "array", + "items": { + "type": "number" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "blob", + "blobs", + "file", + "files", + "title", + "description", + "flags" + ], + "description": "DTO of multipart form data with files' uploading." + } + ], + "output": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "flags": { + "type": "array", + "items": { + "type": "number" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title", + "description", + "flags" + ], + "description": "Content of the multipart form data." + }, + "description": "Upload a multipart data." + }, + { + "method": "get", + "path": "/query/typed", + "name": "query_typed_get", + "parameters": [ + { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + ], + "output": { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + }, + { + "method": "get", + "path": "/query/nest", + "name": "query_nest_get", + "parameters": [ + { + "type": "object", + "properties": { + "limit": { + "type": "string", + "pattern": "^([+-]?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)$" + }, + "enforce": { + "type": "string", + "enum": [ + "false", + "true" + ] + }, + "atomic": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "enforce", + "atomic", + "values" + ], + "description": "Query DTO of NestJS's `@Query()`." + } + ], + "output": { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + }, + { + "method": "get", + "path": "/query/individual", + "name": "query_individual_get", + "parameters": [ + { + "type": "object", + "properties": { + "etc": { + "type": "string", + "description": "" + } + }, + "required": [ + "etc" + ] + } + ], + "output": { + "type": "string" + } + }, + { + "method": "get", + "path": "/query/composite/{id}", + "name": "query_composite_getById", + "parameters": [ + { + "type": "string", + "description": "" + }, + { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "description": "" + } + }, + "required": [ + "enforce", + "values", + "atomic" + ] + } + ], + "output": { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + }, + { + "method": "post", + "path": "/query/body", + "name": "query_body_post", + "parameters": [ + { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + ], + "output": { + "type": "object", + "properties": { + "limit": { + "type": "number" + }, + "enforce": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "atomic": { + "type": "string", + "nullable": true + } + }, + "required": [ + "enforce", + "values", + "atomic" + ], + "description": "Query DTO of `@TypedQuery()`." + } + } + ], + "errors": [], + "options": { + "keyword": false, + "separate": null + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ecec6af..baff5c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -94,6 +94,9 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { "transform": "typia/lib/transform" }, + ], }, "include": ["src"], }