본문으로 건너뛰기
ElastiCORE 3.0 은 릴리스 후보(RC)입니다: Maven Central 의 3.0.261022-rc1. 2.2 라인 프로젝트는 2.2 문서를 보세요.
버전: 3.0

TypeScript 클라이언트

./gradlew elcore3ts 는 서버와 같은 모델에서 화면용 TypeScript 클라이언트를 src/main/ts 에 씁니다: DTO, 라벨이 있는 enum, 노출된 루트마다 타입 있는 API 클라이언트, 서버의 400 과 같은 문구의 검증 함수, 생명주기 버튼용 canX(dto).

./gradlew elcore3ts

build·compileJava 는 이 태스크를 부르지 않습니다. 빌드가 클라이언트도 쓰게 하려면:

build.gradle
elasticore {
architecture = 'hexagonal'
typescriptClient = true // 기본 false. elcore3 다음, 컴파일 전에 elcore3ts 를 실행
}

이 설정이 없는데 src/main/ts 에 예전 모델로 만든 생성 클라이언트가 있으면, elcore3 가 끝에 한 번 경고합니다: "... holds a generated TypeScript client made from an older model - run the elcore3ts task after a model change, or set typescriptClient = true ...".

생성되는 것 (Pet Store)​

src/main/ts/
ItemDTO.ts, ItemSrchDTO.ts, ItemKeyDTO.ts 인터페이스, 필드 제한, validateItem()
ItemApi.ts create, findById, search, update, ... 엔드포인트마다
PurchaseOrderApi.ts ship, cancel, canShip, canCancel, canDelete
CustomerApi.ts signIn, signOut, me (@signin 에서)
OrderStatus.ts, UserRole.ts, CreditCardType.ts enum 과 라벨
Address.ts, CreditCard.ts 값 객체
PurchaseOrderInvariants.ts violations(), isValid() - 보내기 전 확인
apiSupport.ts request(), Page<T>, ProblemDetail, ApiError

unitCost: money(10.2)! @min(0.01, 'price must be positive') 에서 나온 DTO 와 검증:

src/main/ts/ItemDTO.ts (생성)
export interface ItemDTO {
productName?: string;
itemId?: number;
name: string;
description: string;
imagePath?: string;
unitCost: number;
productId?: number;
}

/** Each field's label and limits, as the DSL declares them. */
export const ItemFields = {
name: { label: "name", required: true, maxLength: 30 },
unitCost: { label: "unitCost", required: true, min: 0.01 },
// ...
} as const;

export function validateItem(value: Partial<ItemDTO> | null | undefined, partial = false): Record<string, string> {
// ...
if (!partial && value.unitCost == null) errors.unitCost = "unitCost is required";
else if (value.unitCost != null && Number(value.unitCost) < 0.01) errors.unitCost = "price must be positive";
return errors;
}

화면이 보여 줄 라벨까지 가진 enum:

src/main/ts/OrderStatus.ts (생성)
export const OrderStatus = {
PLACED: "PLC",
SHIPPED: "SHP",
CANCELLED: "CAN"
} as const;

export const OrderStatusLabel: Record<OrderStatus, string> = {
[OrderStatus.PLACED]: "Placed",
[OrderStatus.SHIPPED]: "Shipped",
[OrderStatus.CANCELLED]: "Cancelled",
};

생명주기의 버튼:

src/main/ts/PurchaseOrderApi.ts (생성)
export function canCancel(value: PurchaseOrderDTO | null | undefined): boolean {
return value != null && (value.status === OrderStatus.PLACED);
}

오류​

2xx 가 아닌 응답은 서버의 problem detail 을 담은 ApiError 가 됩니다: 분기용 code(not-found, invalid, state.PurchaseOrder.status.cancel ...)와 폼용 fieldErrors(필드 -> 메시지).

try {
await Orders.cancel(id);
} catch (e) {
if (e instanceof ApiError) showError(e.problem?.detail, e.fieldErrors);
}

로그인은 쿠키 세션이라 화면과 API 가 같은 출처(origin)일 때 동작합니다.

브라우저에 닿는 것​

TypeScript 모델은 서버가 고른 규칙을 따릅니다: Java·Kotlin 규칙이 ID 참조(customerId)와 값 객체 인라인 (deliveryAddress)을 쓰면 TS 도 그렇습니다. XxxApi.ts 는 컨트롤러에 없는 엔드포인트(@readonly, @restExclude, 명령만 받는 루트의 update)를 만들지 않고, @writeonly 필드는 응답에 없으니 optional 입니다. 명세와 불변식은 함수로 따라옵니다 - 강제가 아니라 보내기 전 확인이고, 거절은 여전히 서버가 합니다. 금액은 number 로 선언되고, 생성된 비교는 Number(...) 를 써서 서버가 소수를 JSON 숫자로 보내든 문자열로 보내든 맞습니다.

Pet Store 의 화면(web/src)은 생성된 라벨(OrderStatusLabel), 검증(validateAddress), 서버 오류 (ApiError.fieldErrors), 버튼 판단(canCancel)을 쓰고, webBuild 태스크가 클라이언트에 대해 타입 검사를 합니다.

서버와 화면이 같은 DSL 에서 나오므로, 필드를 바꾸면 화면 코드가 컴파일 오류로 알려 줍니다.