@todo
향후 서비스 공개시, 영어로 다시 작성해야 함@todo
개요에서 다루는 내용들에 대한 상세 기술 문서가 필요
Connector Backend Server.
- Provides functions and metadata for Studio
- Studio is a visual compiler based service
- Workflow is a document of visual components
"(주)뤼튼테크놀로지스" 에서는 "스튜디오" 라고 하여, LLM (Large Language Model) 과 결합한 비쥬얼 컴파일러 기반 서비스가 존재한다. 그리고 이 "스튜디오" 에서 만들 수 있는 비쥬얼 컴포넌트 문서를 "워크플로우" 라고 하는데, "스튜디오" 는 이 "워크플로우 문서" 를 컴파일하여 실행 가능한 프로그램을 만들 수 있다.
그리고 "워크플로우" 에 사용할 수 있는 각각의 함수 노드들은, 대체로 본 커넥터 백엔드 서버로부터 유래한다. "이메일 발송하기" 나 "논문 요약하기" 등의 "워크플로우" 에 배치할 수 있는 기능들이, 바로 본 커넥터 서버에서 제공하는 함수들의 가장 대표적인 사례이다.
또한 "스튜디오" 의 비쥬얼 컴파일러 시스템은 "워크플로우" 를 구성하는 각각의 함수 및 데이터들의 메타데이터를 OpenAPI v3.1 (구 명칭 스웨거) 스펙으로 정의하여 관리하고 있다. 따라서 본 커넥터는 "스튜디오" 에 함수를 제공하는 provider 이면서 동시에, 스웨거 문서를 빌드하여 그것들의 메타데이터 스펙을 전달해주는 역할을 겸하고 있다.
NodeJS v20 혹은 그 이상 버전을 설치한다.
git clone https://github.com/wrtnio/connectors
cd connectors
git update-index --assume-unchanged .env
npm install
npm run build
npm run test
커넥터 서버는 위 git clone
명령어로 설치할 수 있다. 그리고 해당 폴더로 이동하여, git update-index --assume-unchanged .env
명령어를 실행해주자. 이는 앞으로 .env
파일에 그 어떠한 변경이 생기더라도, 이를 커밋하지 않겠다는 명령어이다.
이후 .env 파일을 열어, 각각의 항목들을 채워주도록 한다. .env 파일에 설정할 것들로는 OpenAI 나 AWS S3 인증키 등이 있는데, 로컬에의 설정은 개발자들이 각 서비스로부터 API 인증키 등을 발급받아 재량껏 설정하도록 한다. 참고로 Github Actions 나 실제 서비스에 사용되는 환경변수 값들은 따로 있다.
마지막 npm run build
및 npm run test
를 실행함으로써, 커넥터 서버의 정상 동작 여부를 판별할 수 있다.
# Swagger 문서 단독 빌드시
npm run build:swagger
# Swagger 문서 빌드 후 Swagger-UI 까지 함께 보기
npm run start:swagger
# Swagger 문서 빌드 없이 Swagger-UI 단독 실행
npm run start:swagger -- --skipBuild
위 명령어를 실행하여, Swagger (OpenAPI v3.1) 문서를 빌드하고, swagger-ui
로 열람할 수 있다.
본 프로젝트의 목적이 커넥터 함수의 개발에 더불어, 이를 OpenAPI 스펙의 문서로 빌드하여 뤼튼의 비쥬얼 컴파일러에서 사용할 수 있도록 하는 것이기에, 필히 양질의 API 설계 및 (주석) 문서화 수준을 달성해야하며, 이를 Swagger 문서로 상시 활용/검증할 것이다.
커넥터 서버를 어떻게 개발할 지에 대한 지침.
- API 함수 및 DTO 의 메타데이터를 먼저 정의한다
- 이를 클라이언트용 SDK 라이브러리로 빌드함
- SDK 라이브러리를 활용하여 e2e 테스트 함수를 작성함
- 개별 API 수준에서 각각의 정상 동작 여부를 검증
- 유즈케이스 시나리오에 입각하여 API 함수들을 조합해가며 작성
- 메인 프로그램을 개발하고, 앞서의 테스트 프로그램으로 상시 검증
Define API operation and DTO structures
커넥터 서버를 개발할 때, 가장 먼저 해야 할 일은 API 함수 및 DTO 구조 등의 인터페이스를 정의하는 것이다. 그리고 여기서 말하는 인터페이스 정의란, 메인 프로그램까지 모두 완성하는 것이 아니라, 오직 컨트롤러 메서드 및 DTO 등의 메타데이터 정의까지만을 뜻한다.
아래 예제 코드의 경우, 게시판에 글을 등록하고 조회하는 API 를 형상화한 컨트롤러이다. 보다시피 오직 DTO 타입과 컨트롤러 메서드의 인터페이스만이 정의되었을 뿐, 각각의 컨트롤러 메서드는 모두 그 속이 비었고, 서비스 프로바이더 같은 것은 일절 존재하지 않는다.
이외에 각각의 API 함수 (컨트롤러 메서드) 및 DTO 타입 및 속성들에는, 필요 충분한 만큼의 설명을 주석으로 적어주어야 한다. API 메타데이터에 적힌 제목 및 서술문들이 그대로 OpenAPI 에 구성된 각 API 함수들의 고유 메타데이터 스펙과 함께 LLM (Large Language Model) 에 Function Call (사용자가 LLM 에 제공하여 호출을 유도하는 커스텀 함수 집합) 의 형태로 제공되어, ChatGPT 등이 유저와 전개해나가는 대화의 품질 및 적합 함수 선정에 수준에 지대한 영향을 미치기 때문이다.
@Controller("bbs/articles")
export class BbsArticlesController {
/**
* Get an article with detailed info.
*
* Open an article with detailed info, increasing reading count.
*
* @param section Target section
* @param id Target articles id
* @returns Detailed article info
*/
@core.TypedRoute.Get(":id")
public at(
@core.TypedParam("section") section: string,
@core.TypedParam("id") id: string,
): Promise<IBbsArticle> {
section;
id;
return null!;
}
/**
* Create a new article.
*
* Create a new article and returns its detailed record info.
*
* @param section Target section
* @param input New article info
* @returns Newly created article info
*/
@core.TypedRoute.Post()
public create(
@core.TypedParam("section") section: string,
@core.TypedBody() input: IBbsArticle.ICreate,
): Promise<IBbsArticle> {
section;
input;
return null!;
}
}
npm run build:sdk
API 및 DTO 인터페이스 정의가 완료되거든, 위 명령어를 실행하여 SDK 라이브러리를 빌드해준다.
참고로 여기서 말하는 SDK (Software Development Kit) 라이브러리란, 귀하가 커넥터 서버에 작성한 컨트롤러 메서드와 DTO 타입들을 nestia
가 컴파일러 수준에서 분석, 아래와 같이 클라이언트가 사용할 수 있는 연동 라이브러리로 만들어주는 것을 뜻한다.
Rest API 서버에 대한 일종의 클라이언트 수준 RPC (Remote Procedure Call) 셋인 것.
/**
* Get an article with detailed info.
*
* Open an article with detailed info, increasing reading count.
*
* @param section Target section
* @param id Target articles id
* @returns Detailed article info
*
* @controller BbsArticlesController.at
* @path GET /bbs/articles/:section/:id
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function at(
connection: IConnection,
section: string,
id: string,
): Promise<at.Output> {
return !!connection.simulate
? at.simulate(connection, section, id)
: PlainFetcher.fetch(connection, {
...at.METADATA,
path: at.path(section, id),
});
}
export namespace at {
export type Output = IBbsArticle;
export const METADATA = {
method: "GET",
path: "/bbs/articles/:section/:id",
request: null,
response: {
type: "application/json",
encrypted: false,
},
status: null,
} as const;
export const path = (section: string, id: string) =>
`/bbs/articles/${encodeURIComponent(section ?? "null")}/${encodeURIComponent(id ?? "null")}`;
export const random = (g?: Partial<typia.IRandomGenerator>): IBbsArticle =>
typia.random<IBbsArticle>(g);
export const simulate = (
connection: IConnection,
section: string,
id: string,
): Output => {
const assert = NestiaSimulator.assert({
method: METADATA.method,
host: connection.host,
path: path(section, id),
contentType: "application/json",
});
assert.param("section")(() => typia.assert(section));
assert.param("id")(() => typia.assert(id));
return random(
"object" === typeof connection.simulate && null !== connection.simulate
? connection.simulate
: undefined,
);
};
}
/**
* Create a new article.
*
* Create a new article and returns its detailed record info.
*
* @param section Target section
* @param input New article info
* @returns Newly created article info
*
* @controller BbsArticlesController.create
* @path POST /bbs/articles/:section
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function create(
connection: IConnection,
section: string,
input: IBbsArticle.ICreate,
): Promise<create.Output> {
return !!connection.simulate
? create.simulate(connection, section, input)
: PlainFetcher.fetch(
{
...connection,
headers: {
...connection.headers,
"Content-Type": "application/json",
},
},
{
...create.METADATA,
path: create.path(section),
},
input,
);
}
export namespace create {
export type Input = IBbsArticle.ICreate;
export type Output = IBbsArticle;
export const METADATA = {
method: "POST",
path: "/bbs/articles/:section",
request: {
type: "application/json",
encrypted: false,
},
response: {
type: "application/json",
encrypted: false,
},
status: null,
} as const;
export const path = (section: string) =>
`/bbs/articles/${encodeURIComponent(section ?? "null")}`;
export const random = (g?: Partial<typia.IRandomGenerator>): IBbsArticle =>
typia.random<IBbsArticle>(g);
export const simulate = (
connection: IConnection,
section: string,
input: IBbsArticle.ICreate,
): Output => {
const assert = NestiaSimulator.assert({
method: METADATA.method,
host: connection.host,
path: path(section),
contentType: "application/json",
});
assert.param("section")(() => typia.assert(section));
assert.body(() => typia.assert(input));
return random(
"object" === typeof connection.simulate && null !== connection.simulate
? connection.simulate
: undefined,
);
};
}
위 SDK 라이브러릴 활용, e2e 테스트 프로그램을 작성하면 된다.
각각의 테스트 프로그램은 test/features/api
폴더 및 아무 곳에나 생성하면 된다. 테스트 대상 함수는 export
지시어를 붙이고 test_
의 이름으로 시작하면 되며, 파라미터로는 커넥터 서버로의 접속 정보에 해당하는 IConnection
타입의 변수를 가지면 된다. 그리고 테스트 함수 본문의 코드는, 앞 단원에서 빌드한 SDK 라이브러리를 활용하여 E2E (End to End) 형식으로 작성하면 된다.
참고로 귀하가 작성할 테스트 프로그램은 반드시, 커넥터 서버에 존재하는 모든 API 들을 검증할 수 있어야 한다. 반대로 말하면, 3.1. Definition 단원에서 각각 API 함수들을 정의할 때마다, 3.2. Software Development Kit 을 빌드하고, 이를 통하여 e2e 함수를 작성해야 한다.
이외에 각각의 API 함수들에 대하여 테스트 함수를 각각 작성하는 것도 좋지만, 본래 본 커넥터 서버에 API 함수들을 추가할 때는, 모름지기 소기의 목적이 있는 법이다. 그리고 그 소기의 목적이란 대체로, 단 하나의 API 함수만을 사용하지 않으며, 복수의 API 함수들을 조합하여 활용하기 마련이다.
따라서 e2e 테스트 함수 중에, 이런 식으로 특수 유즈케이스를 가정하고 복수의 SDK 함수를 호출하는 시나리오 형태의 것을 작성하는 일 또한 필요하다. 유즈케이스 시나리오에 입각한 e2e 테스트 프로그램을 작성하는 중, 잘못된 API 설계들이 두드러지게 눈에 띄기 때문에, 이 또한 반드시 필요한 과정 중 하나.
import { RandomGenerator, TestValidator } from "@nestia/e2e";
import typia from "typia";
import { v4 } from "uuid";
import CApi from "@wrtn/connector-api/lib/index";
import { IBbsArticle } from "@wrtn/connector-api/lib/structures/bbs/IBbsArticle";
export async function test_api_bbs_article_store(
connection: CApi.IConnection,
): Promise<void> {
// STORE A NEW ARTICLE
const stored: IBbsArticle = await CApi.functional.bbs.articles.create(
connection,
"general",
{
writer: RandomGenerator.name(),
title: RandomGenerator.paragraph(3)(),
body: RandomGenerator.content(8)()(),
format: "txt",
files: [
{
name: "logo",
extension: "png",
url: "https://somewhere.com/logo.png",
},
],
password: v4(),
},
);
typia.assertEquals(stored);
// READ THE DATA AGAIN
const read: IBbsArticle = await CApi.functional.bbs.articles.at(
connection,
stored.section,
stored.id,
);
typia.assertEquals(read);
TestValidator.equals("write and read")(stored)(read);
}
메인 프로그램의 개발은 앞서의 모든 과정들을 (인터페이스 정의 -> SDK 빌드 -> e2e 테스트 프로그램 작성) 모두 끝낸 다음에야 비로소 진행하는 것으로 한다. 이를 CDD (Contract Driven Development) 내지 TDD (Test Driven Development) 라고 하는데, 본 커넥터 서버와 같은 프로젝트에 특히 유효한 방법론이다.
사전에 인터페이스를 깐깐히 정의하고, 그것에 대한 테스트 프로그램을 미리 준비해둔 끝에 메인 프로그램을 개발함으로써, 각각 개발하는 요소 요소들의 안정성을 상시 보장할 수 있게 되기 때문이다. 두서없이 메인 프로그램부터 개발하다가 설계에 오점을 발견하여 break change 가 생긴다던가, 테스트 프로그램을 생략하여 매번 사람이 손으로 한 땀 한 땀 검증한다던가 하는 일이 없어져 매우 효율적이니, 필히 이 방법론을 따를 것.
개발해야 할 요소 요소들의 (인터페이스 설계 > 테스트 프로그램 준비 > 메인 프로그램) 개발을 마쳤다면, 아래 명령어를 실행함으로써 각 기능이 정상 동작하는 지 확인해 볼 수 있다.
npm run build:sdk
npm run build:test
npm run test
그리고 만일 npm run build:test
대신 npm run dev
명령어를 실행한다면, 테스트 프로그램에 대한 incremental build (프로그램 코드가 수정될 때마다 수정된 내역만 부분 컴파일하여 빌드 결과물의 최신성을 상시 유지하는 방법) 가 실행된다. 그리고 테스트 프로그램을 구동해야 될 때면, 별개의 터미널을 실행하여 npm run test
명령어를 바로 실행해주면 된다. 만일 특정 테스트 함수만 실행하거나 또는 배제하거나 하고 싶다면, 아래 명령어와 같이 --include
와 --exclude
옵션을 사용하면 된다.
# Terminal 1
npm run build:sdk
npm run dev
# Terminal 2
npm run test
npm run test -- --include google daum
npm run test -- --exclude rag hwp youtube
npm run test -- --include google naver --exclude drive
title/summary
와 description
주석을 충실히 작성하자.
커넥터 서버에서 각 DTO 타입 및 컨트롤러 메서드에 작성한 주석은 그대로 OpenAPI 스펙의 title
(또는 summary
) 및 description
의 속성으로써 기록된다.
그리고 이는 다시, OpenAPI 에 구성된 각 API 함수들의 고유 메타데이터 스펙과 함께, LLM (Large Language Model) 에 Function Call (사용자가 LLM 에 제공하여 호출을 유도하는 커스텀 함수 집합) 의 형태로 제공되어, ChatGPT 등이 유저와 전개해나가는 대화의 품질 및 적합 함수 선정에 수준에 지대한 영향을 미친다.
따라서 API 고유 스펙과 더불어 주석은 LLM 세션의 퀄리티에 크게 영향을 주는 바, 각각의 API 함수 (컨트롤러 메서드) 및 DTO 타입 및 속성들에는, 필요 충분한 만큼의 설명을 주석으로 반드시 적어주어야 한다.
/**
* 구글 드라이브에의 이미지 업로드 DTO.
*
* 구글 드라이브에 단일 이미지를 업로드할 때 사용하는 DTO. 만일 복수의 이미지를
* 동시에 업로드하고 싶다면, `IGoogleDriveImageMultipleUpload` DTO 및 관련
* API 함수를 사용하도록 할 것.
*
* @author Jaxtyn
*/
export interface IGoogleDriveImageSingleUpload {
/**
* 구글 사용자 인증 키.
*
* 구글 드라이브에 이미지 파일을 업로드하기 위하여, 구글 사용자 인증이 선행되어야 한다.
* 본 필드값에는, 바로 그 사전 인증하여 발급받은 사용자 인증 키를 할당해주어야 함.
* 그리고 그 인증 키는, read 및 write scope 에 대하여 대응 가능하여야 한다.
*/
token: string & SecretKey<"google-auth-key", ["read", "write"]>;
/**
* 이미지 파일 경로.
*
* Workflow Editor 상 Inspector 내지 Chat Agent 의 File Uploader 의하여 구성됨.
*/
url: string & tags.Format<"uri"> & (
| tags.MediaContentType<"image/png">
| tags.MediaContentType<"image/jpg">
);
/**
* 이미지 파일이 위치할 경로, 파일명 및 확장자는 제외.
*
* @title 파일 경로
*/
location: string;
/**
* 파일명.
*
* 확장자가 제외된, 순수 파일명.
*
* {@link url} 의 실제 파일명과 다르게 업로드 가능.
*/
name: string & Placeholder<"파일명을 입력해주세요.">;
/**
* 이미지 확장자.
*/
extension: "jpg" | "png";
}
DTO 타입을 정의할 때, 위와 같이 각 타입 및 속성별로 설명을 자세히 적어주도록 한다.
그리고 본 커넥터 프로젝트를 개발하다보면, 외부 서비스의 인증 키를 지칭하는 SecretKey
나 UI component 에 힌트로 제공되는 Placeholder
등, 스튜디오 전용 디코레이터 타입을 써야하는 경우가 왕왕 있다. https://github.com/wrtnio/decorators 를 방문하여, 그 사용법을 파악해 둘 것.
참고로 DTO 의 경우에는 JSON Schema 정의상 그 서술부가 title
과 description
으로 나뉘는데, title
는 주석의 가장 하부에 @title Text
라는 명시적인 형태로 작성할 수 있다. 만약 @title
JSDoc 태그가 없다면, 주석 문장의 가장 첫 줄이 온점 (.
) 으로 끝나는 경우 이 것이 title
이 되고, 그렇지 않다면 undefined
가 된다.
@Controller("google/:accountCode/drives/images/upload")
export class GoogleDriveImageUploadController {
/**
* 단일 이미지 파일 업로드.
*
* 단 하나의 이미지 파일을 구글 드라이브에 개별 업로드한다.
*
* @param accountCode 구글 계정명
* @param input 단일 이미지 파일 업로드 정보
* @returns 업로드 완료된 구글 드라이브 파일 정보
*
* @tag Google
*/
@RouteIcon("https://somewhere.com/icons/file.png")
@TypedRoute.Post("single")
public async single(
@SelectorParam(() => GoogleAccountController.prototype.index)
@TypedParam("accountCode")
accountCode: string,
@TypedBody() input: IGoogleDriveImageSingleUpload
): Promise<IGoogleDriveFile> {
...
}
/**
* 복수의 이미지 파일들을 구글 드라이브에 한꺼번에 업로드한다.
*
* @summary 다중 이미지 파일 업로드
* @param accountCode 구글 계정명
* @param input 복수 이미지 파일 업로드 정보
* @returns 업로드 완료된 구글 드라이브 파일들의 정보 리스트
*
* @tag Google
*/
@RouteIcon("https://somewhere.com/icons/file.png")
@TypedRoute.Post("multiple")
public multiple(
@SelectorParam(() => GoogleAccountController.prototype.index)
@TypedParam("accountCode")
accountCode: string,
@TypedBody() input: IGoogleDriveImageMultipleUpload
): Promise<IGoogleDriveFile[]> {
...
}
}
API 컨트롤러 메서드를 정의할 때, 위와 같이 그 기능에 대하여 상세히 적어주도록 한다.
그리고 본 커넥터 프로젝트를 개발하다보면, 각 커넥터 함수의 아이콘을 지칭하는 @RouteIcon()
이나 파라미터를 구성함에 있어 그 리스트를 가져올 수 있는 API 를 지칭하는 @SelectorParam()
등, 스튜디오 전용 디코레이터 타입을 써야하는 경우가 왕왕 있다. https://github.com/wrtnio/decorators 를 방문하여, 그 사용법을 파악해 둘 것.
참고로 API operation 의 경우에는 OpenAPI 스펙 정의상 그 서술부가 summary
과 description
으로 나뉘는데, summary
는 주석의 하부에 @summary Text
라는 명시적인 형태로 작성할 수 있다. 만약 @summary
JSDoc 태그가 없다면, 주석 문장의 가장 첫 줄이 온점 (.
) 으로 끝나는 경우 이 것이 summary
이 되고, 그렇지 않다면 undefined
가 된다.
현 커넥터 프로젝트에서 제공하는 NPM 명령어 모음.
만일 pakage.json
에 새 명령어를 추가하거나 수정하였다면, 필히 본 문서를 수정할 것.
- Test
test
: 테스트 프로그램 실행,build:test
내지dev
후에 실행 가능test:inhouse
: 뤼튼 인프라 내에서만 실행 가능한 테스트 프로그램
- Build
build
: 아래 SDK/Test/Main 프르그램들을 모두 빌드함build:sdk
: SDK 라이브러리 빌드, 로컬 전용build:main
: 메인 프로그램 빌드build:test
: 테스트 프로그램 빌드dev
: 테스트 프로그램의 incremental 빌더eslint
: EsLint 검사기pretter
: 프리티어 일괄 적용
- Deploy
package:api
: SDK 라이브러리 빌드 후 NPM 패키지 배포하기start
: 백엔드 서버 단독 실행기start:swagger
: Swagger UI 실행기
본 커넥터 프로젝트는 대략 아래와 같은 폴더 구조를 취함.
- .vscode/launch.json: Configuration for debugging
- packages/: Packages to publish as private npm modules
- packages/api/: Client SDK library for the client developers
- src/: TypeScript Source directory
- src/api/: Client SDK that would be published to the
@wrtn/connector-api
- src/api/functional/: API functions generated by the
nestia
- src/api/structures/: DTO structures
- src/api/functional/: API functions generated by the
- src/controllers/: Controller classes of the Main Program
- src/providers/: Service providers
- src/executable/: Executable programs
- src/api/: Client SDK that would be published to the
- test/: Test Automation Program
- test/features/api: List of test functions
https://github.com/wrtnio/decorators
스튜디오 시스템에서는 OpenAPI v3.1 및 JSON Schema 의 표준 스펙으로 충족할 수 없는 기능들에 대하여, 별도의 메타데이터 플러그인 속성을 정의하여 이를 벌충하고 있다. 그리고 이 메타데이터 플러그인 속성들을 정의할 수 있는 라이브러리가 바로 @wrtn/decorators
이다.
따라서 위 저장소를 방문, 각각 어느 형태의 메타데이터 플러그인 속성들이 있고 그들의 목적 및 사용법에 대하여 파악토록 하자.
본래 NestJS 는 DTO 를 정의할 때, 이를 반드시 클래스로 선언해야하며, 각각 TypeScript 타입과 validator 와 transformer 및 OpenAPI Spec (JSON Schema) 을 4 중으로 중복 정의해야 한다. 그 과정에서 무수한 사람의 실수가 발생할 수 있어, 메타데이터의 정합성을 보장할 수 없게 된다.
이에 본 커넥터 서버는 NestJS 에 nestia
라는 것을 씌워 사용함으로써, DTO 를 정의할 때 순수 TypeScript 타입을 사용할 수 있게 하였다. 그리고 validation 과 OpenAPI (JSON schema) spec 정의 또한 TypeScript 타입으로부터 컴파일러 수준에서 자동 구성되게 함으로써, 메타데이터에 대한 중복 정의의 필요성 자체를 없애서, 그 안전성을 확보해놨다.
이 점이 커넥터 백엔드 서버가 통상적인 NestJS 백엔드 서버의 개발 방법과 가장 크게 다른 점이니, 필히 유념하도록 하자.
//----------------------------------------------------------
// NestJS 의 전통적인 DTO 정의법
//----------------------------------------------------------
export class BbsArticle {
@ApiProperty({
format: "uuid",
})
@IsString()
id!: string;
// DUPLICATED SCHEMA DEFINITION
// - duplicated function call + property type
// - have to specify `isArray` and `nullable` props by yourself
@ApiProperty({
type: () => AttachmentFile,
nullable: true,
isArray: true,
description: "List of attached files.",
})
@Type(() => AttachmentFile)
@IsArray()
@IsOptional()
@IsObject({ each: true })
@ValidateNested({ each: true })
files!: AttachmentFile[] | null;
@ApiProperty({
type: "string",
nullable: true,
minLength: 5,
maxLength: 100,
description: "Title of the article.",
})
@IsOptional()
@IsString()
title!: string | null;
@ApiProperty({
description: "Main content body of the article.",
})
@IsString()
body!: string;
@ApiProperty({
format: "date-time",
description: "Creation time of article",
})
@IsString()
created_at!: string;
}
//----------------------------------------------------------
// 커넥터 서버는 순수 인터페이스만으로도 DTO 정의 가능
//----------------------------------------------------------
export interface IBbsArticle {
/**
* Primary Key.
*/
id: string & tags.Format<"uuid">;
/**
* List of attached files.
*/
files: null | IAttachmentFile[];
/**
* Title of the article.
*/
title: null | (string & tags.MinLength<5> & tags.MaxLength<100>);
/**
* Main content body of the article.
*/
body: string;
/**
* Creation time of article.
*/
created_at: string & tags.Format<"date-time">;
}