@zipbul/baker 1.0.0 → 2.0.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/README.ko.md DELETED
@@ -1,588 +0,0 @@
1
- <p align="center">
2
- <h1 align="center">@zipbul/baker</h1>
3
- <p align="center">
4
- <strong>데코레이터 기반 validate + transform — 인라인 코드 생성</strong>
5
- </p>
6
- <p align="center">
7
- 단일 <code>@Field()</code> 데코레이터 &middot; AOT급 성능 &middot; reflect-metadata 불필요
8
- </p>
9
- <p align="center">
10
- <a href="https://github.com/zipbul/baker/actions"><img src="https://github.com/zipbul/baker/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/v/@zipbul/baker.svg" alt="npm version"></a>
12
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/dm/@zipbul/baker.svg" alt="npm downloads"></a>
13
- <a href="https://github.com/zipbul/baker/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zipbul/baker.svg" alt="license"></a>
14
- </p>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="./README.md">English</a>
19
- </p>
20
-
21
- ---
22
-
23
- ## 왜 Baker인가?
24
-
25
- | | class-validator | Zod | TypeBox | **Baker** |
26
- |---|---|---|---|---|
27
- | 스키마 방식 | 데코레이터 | 함수 체이닝 | JSON Schema 빌더 | **단일 `@Field()` 데코레이터** |
28
- | 성능 | 런타임 인터프리터 | 런타임 인터프리터 | JIT 컴파일 | **`new Function()` 인라인 코드생성** |
29
- | Transform 내장 | 별도 패키지 | `.transform()` | N/A | **통합** |
30
- | reflect-metadata | 필수 | N/A | N/A | **불필요** |
31
- | class-validator 마이그레이션 | — | 전면 재작성 | 전면 재작성 | **거의 그대로** |
32
-
33
- Baker는 검증, 변환, 노출 제어, 타입 힌트를 결합하는 **단일 `@Field()` 데코레이터**를 제공합니다. 첫 사용 시 `new Function()`으로 최적화된 함수를 생성하여 모든 DTO를 자동으로 seal합니다 — **컴파일러 플러그인 없이 AOT 수준의 성능**을 제공합니다.
34
-
35
- ---
36
-
37
- ## 주요 기능
38
-
39
- - **단일 데코레이터** — `@Field()`가 30개 이상의 개별 데코레이터를 대체
40
- - **80개 이상의 내장 규칙** — `isString`, `min()`, `isEmail()` 등을 인자로 조합
41
- - **인라인 코드 생성** — 첫 `deserialize()`/`serialize()` 호출 시 auto-seal로 검증기를 컴파일
42
- - **검증 + 변환 통합** — `deserialize()`와 `serialize()`를 하나의 async 호출로
43
- - **reflect-metadata 불필요** — `reflect-metadata` import 없이 동작
44
- - **순환 참조 감지** — seal 시점에 자동 정적 분석
45
- - **그룹 기반 검증** — `groups` 옵션으로 요청별 다른 규칙 적용
46
- - **커스텀 규칙** — `createRule()`로 코드생성을 지원하는 사용자 정의 검증기 작성
47
- - **JSON Schema 출력** — `toJsonSchema()`로 DTO에서 JSON Schema Draft 2020-12 생성
48
- - **다형성 discriminator** — `@Field({ discriminator })`로 유니온 타입 지원
49
- - **Whitelist 모드** — `configure({ forbidUnknown: true })`로 미선언 필드 거부
50
- - **클래스 상속** — 자식 DTO가 부모 `@Field()` 데코레이터를 자동으로 상속
51
- - **비동기 transform** — transform 함수에 async 사용 가능
52
-
53
- ---
54
-
55
- ## 설치
56
-
57
- ```bash
58
- bun add @zipbul/baker
59
- ```
60
-
61
- > **요구사항:** Bun >= 1.0, tsconfig.json에 `experimentalDecorators: true` 설정
62
-
63
- ```jsonc
64
- // tsconfig.json
65
- {
66
- "compilerOptions": {
67
- "experimentalDecorators": true
68
- }
69
- }
70
- ```
71
-
72
- ---
73
-
74
- ## 빠른 시작
75
-
76
- ### 1. DTO 정의
77
-
78
- ```typescript
79
- import { Field } from '@zipbul/baker';
80
- import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
81
-
82
- class CreateUserDto {
83
- @Field(isString)
84
- name!: string;
85
-
86
- @Field(isInt, min(0), max(120))
87
- age!: number;
88
-
89
- @Field(isEmail())
90
- email!: string;
91
- }
92
- ```
93
-
94
- ### 2. Deserialize (첫 호출 시 auto-seal)
95
-
96
- ```typescript
97
- import { deserialize, BakerValidationError } from '@zipbul/baker';
98
-
99
- try {
100
- const user = await deserialize(CreateUserDto, requestBody);
101
- // user는 검증 완료된 CreateUserDto 인스턴스
102
- } catch (e) {
103
- if (e instanceof BakerValidationError) {
104
- console.log(e.errors); // BakerError[]
105
- }
106
- }
107
- ```
108
-
109
- ### 3. Serialize
110
-
111
- ```typescript
112
- import { serialize } from '@zipbul/baker';
113
-
114
- const plain = await serialize(userInstance);
115
- // plain: Record<string, unknown>
116
- ```
117
-
118
- > `seal()` 호출이 필요 없습니다 — baker는 첫 `deserialize()` 또는 `serialize()` 호출 시 등록된 모든 DTO를 자동으로 seal합니다.
119
-
120
- ---
121
-
122
- ## `@Field()` 데코레이터
123
-
124
- `@Field()`는 모든 개별 데코레이터를 대체하는 단일 데코레이터입니다. 검증 규칙을 위치 인자로, 고급 기능은 옵션 객체로 전달합니다.
125
-
126
- ### 시그니처
127
-
128
- ```typescript
129
- // 규칙만
130
- @Field(isString, minLength(3), maxLength(100))
131
-
132
- // 옵션만
133
- @Field({ optional: true, nullable: true })
134
-
135
- // 규칙 + 옵션
136
- @Field(isString, { name: 'user_name', groups: ['create'] })
137
-
138
- // 규칙 없이 (단순 필드)
139
- @Field()
140
- ```
141
-
142
- ### FieldOptions
143
-
144
- ```typescript
145
- interface FieldOptions {
146
- type?: () => Constructor | [Constructor]; // 중첩 DTO 타입 (순환 참조를 위한 thunk)
147
- discriminator?: { // 다형성 유니온
148
- property: string;
149
- subTypes: { value: Function; name: string }[];
150
- };
151
- keepDiscriminatorProperty?: boolean; // 출력에 discriminator 키 유지
152
- rules?: (EmittableRule | ArrayOfMarker)[]; // 검증 규칙 (위치 인자 대안)
153
- optional?: boolean; // undefined 허용
154
- nullable?: boolean; // null 허용
155
- name?: string; // JSON 키 매핑 (양방향)
156
- deserializeName?: string; // 역직렬화 전용 키 매핑
157
- serializeName?: string; // 직렬화 전용 키 매핑
158
- exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
159
- groups?: string[]; // 가시성 + 조건부 검증
160
- when?: (obj: any) => boolean; // 조건부 검증
161
- schema?: JsonSchemaOverride; // JSON Schema 메타데이터
162
- transform?: (params: FieldTransformParams) => unknown;
163
- transformDirection?: 'deserializeOnly' | 'serializeOnly';
164
- }
165
- ```
166
-
167
- ### 규칙별 옵션 (message, groups)
168
-
169
- 개별 규칙 함수에 `message`, `groups`, `context`를 직접 전달하는 것은 **불가능**합니다. 대신 `@Field()` 레벨에서 제어합니다:
170
-
171
- - **`groups`** — `FieldOptions.groups`로 설정 (해당 필드의 모든 규칙에 적용)
172
- - **`message`** / **`context`** — `createRule()`로 커스텀 에러 메시지 설정하거나 `BakerError.code`로 처리
173
- - **`each` (배열 요소 검증)** — `arrayOf()` 사용 (아래 참조)
174
-
175
- ### `arrayOf()` — 배열 요소 검증
176
-
177
- `arrayOf()`는 배열의 각 요소에 규칙을 적용합니다. `@zipbul/baker/rules` 또는 `@zipbul/baker`에서 import합니다.
178
-
179
- ```typescript
180
- import { Field, arrayOf } from '@zipbul/baker';
181
- import { isString, minLength } from '@zipbul/baker/rules';
182
-
183
- class TagsDto {
184
- @Field(arrayOf(isString, minLength(1)))
185
- tags!: string[];
186
- }
187
- ```
188
-
189
- `arrayOf()`를 최상위 배열 규칙과 함께 사용할 수 있습니다:
190
-
191
- ```typescript
192
- import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
193
-
194
- class ScoresDto {
195
- @Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
196
- scores!: number[];
197
- }
198
- ```
199
-
200
- ---
201
-
202
- ## 내장 규칙
203
-
204
- 모든 규칙은 `@zipbul/baker/rules`에서 import하며 `@Field()`의 인자로 전달합니다.
205
-
206
- > **상수 vs 팩토리 함수:** 일부 규칙은 미리 만들어진 상수로 `()` 없이 사용하고, 나머지는 매개변수를 받는 팩토리 함수로 `()`와 함께 사용합니다. 아래 표에서 상수는 별도로 표기합니다.
207
-
208
- ### 타입 검사
209
-
210
- | 규칙 | 설명 |
211
- |---|---|
212
- | `isString` | `typeof === 'string'` |
213
- | `isNumber(opts?)` | `typeof === 'number'` + NaN/Infinity/maxDecimalPlaces 검사 |
214
- | `isInt` | 정수 검사 |
215
- | `isBoolean` | `typeof === 'boolean'` |
216
- | `isDate` | `instanceof Date && !isNaN` |
217
- | `isEnum(enumObj)` | 열거형 값 검사 |
218
- | `isArray` | `Array.isArray()` |
219
- | `isObject` | `typeof === 'object'`, null/Array 제외 |
220
-
221
- > `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject`는 상수(괄호 불필요). `isNumber(opts?)`와 `isEnum(enumObj)`는 팩토리 함수.
222
-
223
- ### 공통
224
-
225
- | 규칙 | 설명 |
226
- |---|---|
227
- | `equals(val)` | 엄격 동등 비교 (`===`) |
228
- | `notEquals(val)` | 엄격 비동등 비교 (`!==`) |
229
- | `isEmpty` | `undefined`, `null`, 또는 `''` |
230
- | `isNotEmpty` | `undefined`, `null`, `''`이 아님 |
231
- | `isIn(arr)` | 주어진 배열에 포함 |
232
- | `isNotIn(arr)` | 주어진 배열에 미포함 |
233
-
234
- > `isEmpty`와 `isNotEmpty`는 상수. 나머지는 팩토리 함수.
235
-
236
- ### 숫자
237
-
238
- | 규칙 | 설명 |
239
- |---|---|
240
- | `min(n, opts?)` | `value >= n` (`{ exclusive: true }` 지원) |
241
- | `max(n, opts?)` | `value <= n` (`{ exclusive: true }` 지원) |
242
- | `isPositive` | `value > 0` |
243
- | `isNegative` | `value < 0` |
244
- | `isDivisibleBy(n)` | `value % n === 0` |
245
-
246
- > `isPositive`와 `isNegative`는 상수(괄호 없음). `min()`, `max()`, `isDivisibleBy()`는 팩토리 함수.
247
-
248
- ### 문자열
249
-
250
- 모든 문자열 규칙은 값이 `string` 타입이어야 합니다.
251
-
252
- | 규칙 | 종류 | 설명 |
253
- |---|---|---|
254
- | `minLength(n)` | 팩토리 | 최소 길이 |
255
- | `maxLength(n)` | 팩토리 | 최대 길이 |
256
- | `length(min, max)` | 팩토리 | 길이 범위 |
257
- | `contains(seed)` | 팩토리 | 부분 문자열 포함 |
258
- | `notContains(seed)` | 팩토리 | 부분 문자열 미포함 |
259
- | `matches(pattern, modifiers?)` | 팩토리 | 정규식 매치 |
260
- | `isLowercase` | 상수 | 전체 소문자 |
261
- | `isUppercase` | 상수 | 전체 대문자 |
262
- | `isAscii` | 상수 | ASCII만 |
263
- | `isAlpha` | 상수 | 알파벳만 (en-US) |
264
- | `isAlphanumeric` | 상수 | 알파벳/숫자만 (en-US) |
265
- | `isBooleanString` | 상수 | `'true'`, `'false'`, `'1'`, 또는 `'0'` |
266
- | `isNumberString(opts?)` | 팩토리 | 숫자 문자열 |
267
- | `isDecimal(opts?)` | 팩토리 | 소수 문자열 |
268
- | `isFullWidth` | 상수 | 전각 문자 |
269
- | `isHalfWidth` | 상수 | 반각 문자 |
270
- | `isVariableWidth` | 상수 | 전각/반각 혼합 |
271
- | `isMultibyte` | 상수 | 멀티바이트 문자 |
272
- | `isSurrogatePair` | 상수 | 서로게이트 페어 문자 |
273
- | `isHexadecimal` | 상수 | 16진수 문자열 |
274
- | `isOctal` | 상수 | 8진수 문자열 |
275
- | `isEmail(opts?)` | 팩토리 | 이메일 형식 |
276
- | `isURL(opts?)` | 팩토리 | URL 형식 (포트 범위 검증) |
277
- | `isUUID(version?)` | 팩토리 | UUID v1-v5 |
278
- | `isIP(version?)` | 팩토리 | IPv4 / IPv6 |
279
- | `isHexColor` | 상수 | Hex 색상 (`#fff`, `#ffffff`) |
280
- | `isRgbColor(includePercent?)` | 팩토리 | RGB 색상 문자열 |
281
- | `isHSL` | 상수 | HSL 색상 문자열 |
282
- | `isMACAddress(opts?)` | 팩토리 | MAC 주소 |
283
- | `isISBN(version?)` | 팩토리 | ISBN-10 / ISBN-13 |
284
- | `isISIN` | 상수 | ISIN (국제증권식별번호) |
285
- | `isISO8601(opts?)` | 팩토리 | ISO 8601 날짜 문자열 |
286
- | `isISRC` | 상수 | ISRC (국제표준녹음코드) |
287
- | `isISSN(opts?)` | 팩토리 | ISSN (국제표준일련번호) |
288
- | `isJWT` | 상수 | JSON Web Token |
289
- | `isLatLong(opts?)` | 팩토리 | 위도/경도 문자열 |
290
- | `isLocale` | 상수 | 로케일 문자열 (예: `en_US`) |
291
- | `isDataURI` | 상수 | Data URI |
292
- | `isFQDN(opts?)` | 팩토리 | 정규화된 도메인 이름 |
293
- | `isPort` | 상수 | 포트 번호 문자열 (0-65535) |
294
- | `isEAN` | 상수 | EAN (유럽상품번호) |
295
- | `isISO31661Alpha2` | 상수 | ISO 3166-1 alpha-2 국가 코드 |
296
- | `isISO31661Alpha3` | 상수 | ISO 3166-1 alpha-3 국가 코드 |
297
- | `isBIC` | 상수 | BIC (은행식별코드) / SWIFT 코드 |
298
- | `isFirebasePushId` | 상수 | Firebase Push ID |
299
- | `isSemVer` | 상수 | 시맨틱 버전 문자열 |
300
- | `isMongoId` | 상수 | MongoDB ObjectId (24자 hex) |
301
- | `isJSON` | 상수 | JSON 파싱 가능 문자열 |
302
- | `isBase32(opts?)` | 팩토리 | Base32 인코딩 |
303
- | `isBase58` | 상수 | Base58 인코딩 |
304
- | `isBase64(opts?)` | 팩토리 | Base64 인코딩 |
305
- | `isDateString(opts?)` | 팩토리 | 날짜 문자열 (strict 모드 설정 가능) |
306
- | `isMimeType` | 상수 | MIME 타입 문자열 |
307
- | `isCurrency(opts?)` | 팩토리 | 통화 문자열 |
308
- | `isMagnetURI` | 상수 | Magnet URI |
309
- | `isCreditCard` | 상수 | 신용카드 번호 (Luhn) |
310
- | `isIBAN(opts?)` | 팩토리 | IBAN |
311
- | `isByteLength(min, max?)` | 팩토리 | 바이트 길이 범위 |
312
- | `isHash(algorithm)` | 팩토리 | 해시 문자열 (md4, md5, sha1, sha256, sha384, sha512 등) |
313
- | `isRFC3339` | 상수 | RFC 3339 날짜시간 문자열 |
314
- | `isMilitaryTime` | 상수 | 군사 시간 (HH:MM) |
315
- | `isLatitude` | 상수 | 위도 문자열 |
316
- | `isLongitude` | 상수 | 경도 문자열 |
317
- | `isEthereumAddress` | 상수 | 이더리움 주소 |
318
- | `isBtcAddress` | 상수 | 비트코인 주소 |
319
- | `isISO4217CurrencyCode` | 상수 | ISO 4217 통화 코드 |
320
- | `isPhoneNumber` | 상수 | E.164 국제 전화번호 |
321
- | `isStrongPassword(opts?)` | 팩토리 | 강력한 비밀번호 (최소 길이, 대/소문자, 숫자, 특수문자 설정) |
322
- | `isTaxId(locale)` | 팩토리 | 주어진 로케일의 세금 ID |
323
-
324
- ### 배열
325
-
326
- | 규칙 | 설명 |
327
- |---|---|
328
- | `arrayContains(values)` | 주어진 요소를 모두 포함 |
329
- | `arrayNotContains(values)` | 주어진 요소를 포함하지 않음 |
330
- | `arrayMinSize(n)` | 배열 최소 길이 |
331
- | `arrayMaxSize(n)` | 배열 최대 길이 |
332
- | `arrayUnique()` | 중복 없음 |
333
- | `arrayNotEmpty()` | 빈 배열이 아님 |
334
-
335
- ### 날짜
336
-
337
- | 규칙 | 설명 |
338
- |---|---|
339
- | `minDate(date)` | 최소 날짜 |
340
- | `maxDate(date)` | 최대 날짜 |
341
-
342
- ### 객체
343
-
344
- | 규칙 | 설명 |
345
- |---|---|
346
- | `isNotEmptyObject(opts?)` | 최소 1개의 키 보유 (`{ nullable: true }` 옵션으로 null 값 키 무시) |
347
- | `isInstance(Class)` | 주어진 클래스에 대한 `instanceof` 검사 |
348
-
349
- ### 로케일
350
-
351
- 로케일 문자열 매개변수를 받는 지역별 검증기입니다.
352
-
353
- | 규칙 | 설명 |
354
- |---|---|
355
- | `isMobilePhone(locale)` | 주어진 로케일의 휴대전화 번호 (예: `'ko-KR'`, `'en-US'`, `'ja-JP'`) |
356
- | `isPostalCode(locale)` | 주어진 로케일/국가 코드의 우편번호 (예: `'US'`, `'KR'`, `'GB'`) |
357
- | `isIdentityCard(locale)` | 주어진 로케일의 주민등록번호/신분증 번호 (예: `'KR'`, `'US'`, `'CN'`) |
358
- | `isPassportNumber(locale)` | 주어진 로케일의 여권 번호 (예: `'US'`, `'KR'`, `'GB'`) |
359
-
360
- ---
361
-
362
- ## 설정
363
-
364
- 첫 `deserialize()`/`serialize()` 호출 **이전에** `configure()`를 호출하세요:
365
-
366
- ```typescript
367
- import { configure } from '@zipbul/baker';
368
-
369
- configure({
370
- autoConvert: false, // 암묵적 타입 변환 ("123" -> 123)
371
- allowClassDefaults: false, // 누락된 키에 클래스 기본값 사용
372
- stopAtFirstError: false, // 첫 에러에서 중단 또는 전체 수집
373
- forbidUnknown: false, // 미선언 필드 거부
374
- debug: false, // 생성된 코드에 필드 제외 주석 포함
375
- });
376
- ```
377
-
378
- `configure()`는 `{ warnings: string[] }`을 반환합니다 — auto-seal 이후에 호출된 경우, 영향을 받지 않는 클래스를 알려주는 경고가 포함됩니다.
379
-
380
- ---
381
-
382
- ## 에러 처리
383
-
384
- 검증 실패 시 `deserialize()`는 `BakerValidationError`를 throw합니다:
385
-
386
- ```typescript
387
- class BakerValidationError extends Error {
388
- readonly errors: BakerError[];
389
- readonly className: string;
390
- }
391
-
392
- interface BakerError {
393
- readonly path: string; // 'user.address.city'
394
- readonly code: string; // 'isString', 'min', 'isEmail'
395
- readonly message?: string; // 커스텀 메시지
396
- readonly context?: unknown; // 커스텀 컨텍스트
397
- }
398
- ```
399
-
400
- ---
401
-
402
- ## 중첩 객체
403
-
404
- `type` 옵션으로 중첩 DTO를 검증합니다:
405
-
406
- ```typescript
407
- class AddressDto {
408
- @Field(isString)
409
- city!: string;
410
- }
411
-
412
- class UserDto {
413
- @Field({ type: () => AddressDto })
414
- address!: AddressDto;
415
-
416
- // 중첩 DTO 배열
417
- @Field({ type: () => [AddressDto] })
418
- addresses!: AddressDto[];
419
- }
420
- ```
421
-
422
- ### Discriminator (다형성)
423
-
424
- ```typescript
425
- class DogDto {
426
- @Field(isString) breed!: string;
427
- }
428
- class CatDto {
429
- @Field(isBoolean) indoor!: boolean;
430
- }
431
-
432
- class PetOwnerDto {
433
- @Field({
434
- type: () => DogDto,
435
- discriminator: {
436
- property: 'type',
437
- subTypes: [
438
- { value: DogDto, name: 'dog' },
439
- { value: CatDto, name: 'cat' },
440
- ],
441
- },
442
- })
443
- pet!: DogDto | CatDto;
444
- }
445
- ```
446
-
447
- Discriminator는 양방향으로 동작합니다 — `deserialize()`는 프로퍼티 값으로 분기하고, `serialize()`는 `instanceof`로 분기합니다.
448
-
449
- ---
450
-
451
- ## 상속
452
-
453
- Baker는 클래스 상속을 지원합니다. 자식 DTO는 부모 클래스의 모든 `@Field()` 데코레이터를 자동으로 상속합니다. 자식 클래스에서 필드를 오버라이드하거나 확장할 수 있습니다:
454
-
455
- ```typescript
456
- class BaseDto {
457
- @Field(isString)
458
- name!: string;
459
- }
460
-
461
- class ExtendedDto extends BaseDto {
462
- @Field(isInt, min(0))
463
- age!: number;
464
- // `name`은 BaseDto에서 상속
465
- }
466
- ```
467
-
468
- ---
469
-
470
- ## Transform
471
-
472
- `FieldOptions`의 `transform` 옵션으로 역직렬화/직렬화 시 값을 변환할 수 있습니다. Transform 함수는 **비동기(async)**일 수 있습니다.
473
-
474
- ```typescript
475
- class UserDto {
476
- @Field(isString, {
477
- transform: ({ value, direction }) => {
478
- return direction === 'deserialize'
479
- ? (value as string).trim().toLowerCase()
480
- : value;
481
- },
482
- })
483
- email!: string;
484
-
485
- @Field(isString, {
486
- transform: async ({ value }) => {
487
- return await someAsyncOperation(value);
488
- },
489
- transformDirection: 'deserializeOnly',
490
- })
491
- data!: string;
492
- }
493
- ```
494
-
495
- ---
496
-
497
- ## 커스텀 규칙
498
-
499
- ```typescript
500
- import { createRule } from '@zipbul/baker';
501
-
502
- const isPositiveInt = createRule({
503
- name: 'isPositiveInt',
504
- validate: (value) => Number.isInteger(value) && (value as number) > 0,
505
- });
506
-
507
- class Dto {
508
- @Field(isPositiveInt)
509
- count!: number;
510
- }
511
- ```
512
-
513
- ---
514
-
515
- ## 클래스 레벨 JSON Schema 메타데이터
516
-
517
- `collectClassSchema()`를 사용하여 DTO에 클래스 레벨 JSON Schema 메타데이터(title, description 등)를 부착합니다. 이 메타데이터는 `toJsonSchema()` 출력에 병합됩니다.
518
-
519
- > `collectClassSchema`는 `src/collect.ts`에서 제공하는 저수준 API입니다. 서브패스 익스포트로는 사용할 수 없으며 직접 import해야 합니다.
520
-
521
- ```typescript
522
- import { collectClassSchema } from '@zipbul/baker/src/collect';
523
-
524
- class CreateUserDto {
525
- @Field(isString) name!: string;
526
- @Field(isEmail()) email!: string;
527
- }
528
-
529
- collectClassSchema(CreateUserDto, {
530
- title: 'CreateUserRequest',
531
- description: '새 사용자 생성을 위한 페이로드',
532
- });
533
- ```
534
-
535
- 프로퍼티 레벨 스키마 오버라이드는 `@Field()`의 `schema` 옵션을 사용합니다:
536
-
537
- ```typescript
538
- class Dto {
539
- @Field(isString, minLength(1), {
540
- schema: { description: '사용자 표시 이름', minLength: 5 },
541
- })
542
- name!: string;
543
- }
544
- ```
545
-
546
- ---
547
-
548
- ## JSON Schema
549
-
550
- DTO에서 JSON Schema Draft 2020-12를 생성합니다:
551
-
552
- ```typescript
553
- import { toJsonSchema } from '@zipbul/baker';
554
-
555
- const schema = toJsonSchema(CreateUserDto, {
556
- direction: 'deserialize', // 'deserialize' | 'serialize'
557
- groups: ['create'], // 그룹별 필터링
558
- onUnmappedRule: (name) => { /* 스키마 매핑이 없는 커스텀 규칙 */ },
559
- });
560
- ```
561
-
562
- ---
563
-
564
- ## 동작 원리
565
-
566
- ```
567
- Decorators (@Field) auto-seal (첫 호출) deserialize() / serialize()
568
- 메타데이터 -> new Function() 코드생성 -> 생성된 코드 실행
569
- ```
570
-
571
- 1. `@Field()`가 정의 시점에 클래스 프로퍼티에 검증 메타데이터를 부착합니다
572
- 2. 첫 `deserialize()`/`serialize()` 호출이 **auto-seal**을 트리거합니다 — 모든 메타데이터를 읽고, 순환 참조를 분석하고, `new Function()`으로 최적화된 JavaScript 함수를 생성합니다
573
- 3. 이후 호출은 생성된 함수를 직접 실행합니다 — 해석 루프 없음
574
-
575
- ---
576
-
577
- ## 서브패스 익스포트
578
-
579
- | 임포트 경로 | 용도 |
580
- |---|---|
581
- | `@zipbul/baker` | 메인 API: `deserialize`, `serialize`, `configure`, `toJsonSchema`, `Field`, `arrayOf`, `createRule` |
582
- | `@zipbul/baker/rules` | 규칙 함수 및 상수: `isString`, `min()`, `isEmail()`, `arrayOf()` 등 |
583
-
584
- ---
585
-
586
- ## 라이선스
587
-
588
- [MIT](./LICENSE)
@@ -1,6 +0,0 @@
1
- // @bun
2
- import{e as J}from"./index-aegrb1kn.js";var Q=new Set;function T(b,w){if(!Object.prototype.hasOwnProperty.call(b,J))b[J]=Object.create(null),Q.add(b);let j=b[J];return j[w]??={validation:[],transform:[],expose:[],exclude:null,type:null,flags:{},schema:null}}function U(b){return b[Symbol.toStringTag]==="AsyncFunction"}var Z=Symbol.for("baker:arrayOf");function d(...b){let w={rules:b};return w[Z]=!0,w}function $(b){return typeof b==="object"&&b!==null&&b[Z]===!0}var K=new Set(["type","discriminator","keepDiscriminatorProperty","rules","optional","nullable","name","deserializeName","serializeName","exclude","groups","when","schema","transform","transformDirection"]);function X(b){if(typeof b==="function")return!1;if(typeof b!=="object"||b===null)return!1;if($(b))return!1;let w=Object.keys(b);if(w.length===0)return!0;return w.some((j)=>K.has(j))}function N(b){if(b.length===0)return{rules:[],options:{}};if(b.length===1&&X(b[0])){let j=b[0];return{rules:j.rules??[],options:j}}let w=b[b.length-1];if(X(w)){let j=w,B=b.slice(0,-1);if(j.rules)B=[...B,...j.rules];return{rules:B,options:j}}return{rules:b,options:{}}}function P(b,w,j){for(let B of w)if($(B))for(let G of B.rules)b.validation.push({rule:G,each:!0,groups:j.groups});else b.validation.push({rule:B,groups:j.groups})}function S(b,w){if(w.name)b.expose.push({name:w.name,groups:w.groups});else if(w.deserializeName||w.serializeName){if(w.deserializeName)b.expose.push({name:w.deserializeName,deserializeOnly:!0,groups:w.groups});if(w.serializeName)b.expose.push({name:w.serializeName,serializeOnly:!0,groups:w.groups})}else if(w.groups)b.expose.push({groups:w.groups});else b.expose.push({})}function V(b,w){if(!w.transform)return;let j=w.transform,G=U(j)?async(z)=>j({value:z.value,key:z.key,obj:z.obj,direction:z.type}):(z)=>j({value:z.value,key:z.key,obj:z.obj,direction:z.type});if(w.transformDirection&&w.transformDirection!=="deserializeOnly"&&w.transformDirection!=="serializeOnly")throw Error(`Invalid transformDirection: "${w.transformDirection}". Expected 'deserializeOnly' or 'serializeOnly'.`);let q={};if(w.transformDirection==="deserializeOnly")q.deserializeOnly=!0;if(w.transformDirection==="serializeOnly")q.serializeOnly=!0;b.transform.push({fn:G,options:Object.keys(q).length>0?q:void 0})}function F(...b){return(w,j)=>{let B=w.constructor,q=T(B,j),{rules:z,options:v}=N(b);if(P(q,z,v),v.optional)q.flags.isOptional=!0;if(v.nullable)q.flags.isNullable=!0;if(v.when)q.flags.validateIf=v.when;if(v.type)q.type={fn:v.type,discriminator:v.discriminator,keepDiscriminatorProperty:v.keepDiscriminatorProperty};if(S(q,v),v.exclude){if(v.exclude===!0)q.exclude={};else if(v.exclude==="deserializeOnly")q.exclude={deserializeOnly:!0};else if(v.exclude==="serializeOnly")q.exclude={serializeOnly:!0}}if(V(q,v),v.schema)if(typeof q.schema==="function");else q.schema={...q.schema??{},...v.schema}}}
3
- export{Q as a,U as b,d as c,F as d};
4
-
5
- //# debugId=3AECA7367D7355B264756E2164756E21
6
- //# sourceMappingURL=index-57gr0v18.js.map
@@ -1,6 +0,0 @@
1
- // @bun
2
- var b=Symbol.for("baker:raw"),d=Symbol.for("baker:sealed"),f=Symbol.for("baker:rawClassSchema");
3
- export{b as e,d as f,f as g};
4
-
5
- //# debugId=BEECA922A80577C664756E2164756E21
6
- //# sourceMappingURL=index-aegrb1kn.js.map
@@ -1,20 +0,0 @@
1
- import type { JsonSchema202012 } from '../types';
2
- export interface ToJsonSchemaOptions {
3
- direction?: 'deserialize' | 'serialize';
4
- groups?: string[];
5
- /** true: 모든 object 스키마에 unevaluatedProperties: false 추가 (seal의 whitelist 옵션 대응) */
6
- whitelist?: boolean;
7
- /** 클래스 레벨 JSON Schema 메타데이터 (title, description 등) */
8
- title?: string;
9
- description?: string;
10
- $id?: string;
11
- /** 매핑되지 않은 규칙에 대한 콜백 (기본: console.warn) */
12
- onUnmappedRule?: (ruleName: string, fieldKey: string) => void;
13
- }
14
- /**
15
- * 등록된 DTO 클래스를 JSON Schema Draft 2020-12 형식으로 변환한다.
16
- * - 루트 클래스는 인라인, 중첩 클래스는 $defs에 배치
17
- * - 순환 참조는 $ref로 안전 처리
18
- * - seal() 이전에도 호출 가능 (RAW 메타데이터 직접 사용)
19
- */
20
- export declare function toJsonSchema(Class: Function, options?: ToJsonSchemaOptions): JsonSchema202012;