@fluojs/serialization 1.0.0-beta.4 → 1.0.0-beta.5
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 +53 -68
- package/README.md +7 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +2 -2
- package/package.json +3 -3
package/README.ko.md
CHANGED
|
@@ -4,18 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
fluo를 위한 클래스 기반 응답 직렬화 및 데코레이터 인지형 재귀 출력 가공 엔진입니다.
|
|
6
6
|
|
|
7
|
-
`@fluojs/serialization`은 애플리케이션의 **출력 경계(Output Boundary)**를 담당합니다. 내부 클래스 인스턴스나 복잡한 객체 그래프를 데코레이터 규칙이 반영된 일반 응답 형태로 변환하는 선언적인 방법을 제공합니다. 이를 통해 API 응답에 의도한 데이터만 노출되도록 보장합니다.
|
|
8
|
-
|
|
9
7
|
## 목차
|
|
10
8
|
|
|
11
9
|
- [설치](#설치)
|
|
12
10
|
- [사용 시점](#사용-시점)
|
|
13
11
|
- [빠른 시작](#빠른-시작)
|
|
14
12
|
- [주요 패턴](#주요-패턴)
|
|
15
|
-
- [민감한 데이터 제외](#민감한-데이터-제외)
|
|
16
|
-
- [값 변환 (Transforming)](#값-변환-transforming)
|
|
17
|
-
- [순환 참조 처리](#순환-참조-처리)
|
|
18
|
-
- [HTTP 인터셉터와 함께 사용](#http-인터셉터와-함께-사용)
|
|
19
13
|
- [공개 API 개요](#공개-api-개요)
|
|
20
14
|
- [관련 패키지](#관련-패키지)
|
|
21
15
|
- [예제 소스](#예제-소스)
|
|
@@ -28,94 +22,70 @@ pnpm add @fluojs/serialization
|
|
|
28
22
|
|
|
29
23
|
## 사용 시점
|
|
30
24
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
25
|
+
- output DTO가 제어된 일부 필드만 노출해야 할 때
|
|
26
|
+
- password hash나 내부 identifier 같은 민감한 값이 response boundary를 벗어나면 안 될 때
|
|
27
|
+
- response data가 serialization 중 lightweight synchronous transform을 거쳐야 할 때
|
|
28
|
+
- HTTP interceptor가 같은 serialization rule을 자동으로 적용하게 하고 싶을 때
|
|
35
29
|
|
|
36
30
|
## 빠른 시작
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
32
|
+
```ts
|
|
41
33
|
import { Expose, Exclude, Transform, serialize } from '@fluojs/serialization';
|
|
42
34
|
|
|
43
35
|
class UserEntity {
|
|
44
36
|
@Expose()
|
|
45
|
-
id
|
|
37
|
+
id = '';
|
|
46
38
|
|
|
47
39
|
@Expose()
|
|
48
|
-
@Transform((
|
|
49
|
-
username
|
|
40
|
+
@Transform((value) => value.toUpperCase())
|
|
41
|
+
username = '';
|
|
50
42
|
|
|
51
43
|
@Exclude()
|
|
52
|
-
passwordHash
|
|
53
|
-
|
|
54
|
-
constructor(partial: Partial<UserEntity>) {
|
|
55
|
-
Object.assign(this, partial);
|
|
56
|
-
}
|
|
44
|
+
passwordHash = '';
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
const user = new UserEntity(
|
|
60
|
-
|
|
47
|
+
const user = Object.assign(new UserEntity(), {
|
|
48
|
+
id: '1',
|
|
49
|
+
username: 'fluo',
|
|
50
|
+
passwordHash: 'secret',
|
|
51
|
+
});
|
|
61
52
|
|
|
62
|
-
console.log(
|
|
63
|
-
//
|
|
64
|
-
// passwordHash는 제외됩니다.
|
|
53
|
+
console.log(serialize(user));
|
|
54
|
+
// { id: '1', username: 'FLUO' }
|
|
65
55
|
```
|
|
66
56
|
|
|
67
57
|
## 주요 패턴
|
|
68
58
|
|
|
69
|
-
###
|
|
70
|
-
|
|
71
|
-
`@Exclude()`를 사용하여 특정 속성이 출력에 절대 나타나지 않도록 합니다. 클래스 레벨에서 `@Expose({ excludeExtraneous: true })`를 사용하면 명시적으로 허용된 필드만 포함하는 "화이트리스트" 전략을 구현할 수 있습니다.
|
|
59
|
+
### 노출 전용 출력 DTO
|
|
72
60
|
|
|
73
|
-
```
|
|
74
|
-
import { Expose
|
|
61
|
+
```ts
|
|
62
|
+
import { Expose } from '@fluojs/serialization';
|
|
75
63
|
|
|
76
64
|
@Expose({ excludeExtraneous: true })
|
|
77
65
|
class SecureDto {
|
|
78
66
|
@Expose()
|
|
79
|
-
publicData
|
|
67
|
+
publicData = 'visible';
|
|
80
68
|
|
|
81
|
-
internalData
|
|
69
|
+
internalData = 'hidden';
|
|
82
70
|
}
|
|
83
71
|
```
|
|
84
72
|
|
|
85
|
-
### 값 변환
|
|
73
|
+
### 값 변환
|
|
86
74
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
75
|
+
```ts
|
|
90
76
|
import { Transform } from '@fluojs/serialization';
|
|
91
77
|
|
|
92
78
|
class ProductDto {
|
|
93
79
|
@Transform((price) => `$${price.toFixed(2)}`)
|
|
94
|
-
price
|
|
80
|
+
price = 0;
|
|
95
81
|
}
|
|
96
82
|
```
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
fluo의 직렬화 엔진은 활성 순환 참조를 자동으로 감지하고 `undefined`로 절단하여 무한 루프와 스택 오버플로를 방지합니다. 이미 직렬화가 끝난 공유 참조는 삭제하지 않고 직렬화된 그래프 안에서 재사용합니다. 예를 들어 두 sibling 필드가 같은 원본 객체를 가리키면 두 직렬화 결과도 같은 직렬화 객체를 가리키며, 현재 직렬화 중인 객체를 다시 만나는 활성 cycle만 `undefined`로 절단됩니다.
|
|
101
|
-
|
|
102
|
-
### 상속된 데코레이터 계약
|
|
103
|
-
|
|
104
|
-
기반 클래스에 선언한 직렬화 메타데이터는 파생 DTO에도 상속됩니다. 공통 필드에 적용한 `@Expose()`, `@Exclude()`, `@Transform()` 규칙은 서브클래스 인스턴스를 직렬화할 때도 그대로 반영됩니다.
|
|
105
|
-
|
|
106
|
-
### 일반 객체 안전성
|
|
107
|
-
|
|
108
|
-
`serialize()`는 일반 객체와 null-prototype 레코드를 데코레이터가 붙은 클래스 인스턴스로 오인하지 않습니다. 사용자 정의 `constructor` 필드나 안전하지 않은 `constructor` 값을 가진 객체도 예외 없이 안전하게 순회합니다.
|
|
109
|
-
|
|
110
|
-
### 비JSON leaf 값
|
|
111
|
-
|
|
112
|
-
`serialize()`는 데코레이터 메타데이터를 적용하고 배열/일반 객체를 재귀적으로 순회하지만, 모든 leaf 값을 엄격한 JSON 타입으로 강제 변환하지는 않습니다. `Date`, `Map`, `Set`, `URL`, `URLSearchParams`, `RegExp`, `Error`, `ArrayBuffer`, typed array, `WeakMap`, `WeakSet`, `Promise` 같은 opaque built-in은 DTO 같은 클래스 인스턴스로 펼치지 않고 그대로 통과합니다. `bigint`, 함수, `symbol` 같은 값도 `@Transform(...)`이나 최종 HTTP 응답 작성 전에 직접 정규화하지 않으면 그대로 통과할 수 있습니다.
|
|
84
|
+
같은 필드가 base class와 derived class 모두에서 decorate되면 transform은 base에서 derived 순서로 실행됩니다.
|
|
113
85
|
|
|
114
86
|
### HTTP 인터셉터와 함께 사용
|
|
115
87
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
88
|
+
```ts
|
|
119
89
|
import { Controller, Get, UseInterceptors } from '@fluojs/http';
|
|
120
90
|
import { SerializerInterceptor } from '@fluojs/serialization';
|
|
121
91
|
|
|
@@ -124,30 +94,45 @@ import { SerializerInterceptor } from '@fluojs/serialization';
|
|
|
124
94
|
class UsersController {
|
|
125
95
|
@Get('/')
|
|
126
96
|
findAll() {
|
|
127
|
-
return [new UserEntity(
|
|
97
|
+
return [new UserEntity()];
|
|
128
98
|
}
|
|
129
99
|
}
|
|
130
100
|
```
|
|
131
101
|
|
|
132
102
|
`SerializerInterceptor`는 일반 HTTP 응답 writer가 아직 소유한 값만 직렬화합니다. 핸들러나 응답 헬퍼가 SSE 스트림처럼 `RequestContext.response`를 직접 커밋한 경우, 인터셉터는 해당 핸들러 소유 값을 그대로 반환하여 request pipeline의 응답 소유권을 보존합니다.
|
|
133
103
|
|
|
104
|
+
### 순환 참조 처리
|
|
105
|
+
|
|
106
|
+
fluo의 직렬화 엔진은 활성 순환 참조를 자동으로 감지하고 `undefined`로 절단하여 무한 루프와 스택 오버플로를 방지합니다. 이미 직렬화가 끝난 공유 참조는 삭제하지 않고 직렬화된 그래프 안에서 재사용합니다. 예를 들어 두 sibling 필드가 같은 원본 객체를 가리키면 두 직렬화 결과도 같은 직렬화 객체를 가리키며, 현재 직렬화 중인 객체를 다시 만나는 활성 cycle만 `undefined`로 절단됩니다.
|
|
107
|
+
|
|
108
|
+
### 상속된 데코레이터 계약
|
|
109
|
+
|
|
110
|
+
기반 클래스에 선언한 직렬화 메타데이터는 파생 DTO에도 상속됩니다. 공통 필드에 적용한 `@Expose()`, `@Exclude()`, `@Transform()` 규칙은 서브클래스 인스턴스를 직렬화할 때도 그대로 반영됩니다.
|
|
111
|
+
|
|
112
|
+
Decorated metadata가 없는 class instance도 재귀적으로 순회하므로, parent object에 serialization metadata가 없어도 decorated nested descendant는 반영됩니다.
|
|
113
|
+
|
|
114
|
+
### 일반 객체 안전성
|
|
115
|
+
|
|
116
|
+
`serialize()`는 일반 객체와 null-prototype 레코드를 데코레이터가 붙은 클래스 인스턴스로 오인하지 않습니다. Enumerable symbol key도 직렬화하며, own `__proto__`, `constructor`, `prototype` key는 prototype mutation이 아니라 data로 취급합니다. 사용자 정의 `constructor` 필드나 안전하지 않은 `constructor` 값을 가진 객체도 예외 없이 안전하게 순회합니다.
|
|
117
|
+
|
|
118
|
+
### 비JSON leaf 값
|
|
119
|
+
|
|
120
|
+
`serialize()`는 데코레이터 메타데이터를 적용하고 배열/일반 객체를 재귀적으로 순회하지만, 모든 leaf 값을 엄격한 JSON 타입으로 강제 변환하지는 않습니다. `Date`, `Map`, `Set`, `URL`, `URLSearchParams`, `RegExp`, `Error`, `ArrayBuffer`, typed array, `WeakMap`, `WeakSet`, `Promise` 같은 opaque built-in은 DTO 같은 클래스 인스턴스로 펼치지 않고 그대로 통과합니다. `bigint`, 함수, `symbol` 같은 값도 `@Transform(...)`이나 최종 HTTP 응답 작성 전에 직접 정규화하지 않으면 그대로 통과할 수 있습니다.
|
|
121
|
+
|
|
134
122
|
## 공개 API 개요
|
|
135
123
|
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
- `@Transform(fn)`: 속성에 대한 변환 함수를 등록합니다.
|
|
124
|
+
- **데코레이터**: `Expose`, `Exclude`, `Transform`
|
|
125
|
+
- **엔진**: `serialize(value)`
|
|
126
|
+
- **HTTP 통합**: `SerializerInterceptor`
|
|
140
127
|
|
|
141
|
-
|
|
142
|
-
- `serialize(value)`: 객체/배열을 재귀적으로 순회하며 직렬화 규칙과 데코레이터를 적용합니다.
|
|
143
|
-
- `SerializerInterceptor`: 핸들러의 반환 값에 대해 `serialize`를 호출하는 fluo HTTP 인터셉터입니다.
|
|
128
|
+
`Expose`는 class와 field에 적용할 수 있습니다. `Exclude`와 `Transform`은 field에 적용합니다.
|
|
144
129
|
|
|
145
130
|
## 관련 패키지
|
|
146
131
|
|
|
147
|
-
- `@fluojs/http`: `SerializerInterceptor`를
|
|
148
|
-
- `@fluojs/validation`:
|
|
132
|
+
- `@fluojs/http`: HTTP handler에 `SerializerInterceptor`를 적용합니다.
|
|
133
|
+
- `@fluojs/validation`: input-side DTO materialization과 validation을 담당합니다.
|
|
149
134
|
|
|
150
135
|
## 예제 소스
|
|
151
136
|
|
|
152
|
-
- `packages/serialization/src/serialize.test.ts
|
|
153
|
-
- `packages/serialization/src/serializer-interceptor.test.ts
|
|
137
|
+
- `packages/serialization/src/serialize.test.ts`
|
|
138
|
+
- `packages/serialization/src/serializer-interceptor.test.ts`
|
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ class ProductDto {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
When the same field is decorated in a base class and a derived class, transforms run in declaration order from base to derived.
|
|
85
|
+
|
|
84
86
|
### HTTP response shaping with an interceptor
|
|
85
87
|
|
|
86
88
|
```ts
|
|
@@ -107,9 +109,11 @@ The serializer cuts active cyclic references safely instead of recursing forever
|
|
|
107
109
|
|
|
108
110
|
Serialization metadata declared on a base class is inherited by derived DTOs. `@Expose()`, `@Exclude()`, and `@Transform()` rules applied to shared base fields still take effect when you serialize subclass instances.
|
|
109
111
|
|
|
112
|
+
Undecorated class instances are still traversed recursively, so decorated nested descendants are respected even when the parent object has no serialization metadata.
|
|
113
|
+
|
|
110
114
|
### Plain-object safety
|
|
111
115
|
|
|
112
|
-
`serialize()` treats plain objects and null-prototype records as data containers, not decorated class instances.
|
|
116
|
+
`serialize()` treats plain objects and null-prototype records as data containers, not decorated class instances. Enumerable symbol keys are serialized, own `__proto__`, `constructor`, and `prototype` keys are treated as data rather than prototype mutations, and objects with custom or unsafe `constructor` fields are walked safely without throwing.
|
|
113
117
|
|
|
114
118
|
### Non-JSON leaf values
|
|
115
119
|
|
|
@@ -121,6 +125,8 @@ Serialization metadata declared on a base class is inherited by derived DTOs. `@
|
|
|
121
125
|
- **Engine**: `serialize(value)`
|
|
122
126
|
- **HTTP integration**: `SerializerInterceptor`
|
|
123
127
|
|
|
128
|
+
`Expose` can be applied to classes and fields. `Exclude` and `Transform` apply to fields.
|
|
129
|
+
|
|
124
130
|
## Related Packages
|
|
125
131
|
|
|
126
132
|
- `@fluojs/http`: applies `SerializerInterceptor` to HTTP handlers
|
package/dist/metadata.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKxD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKxD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAClC;AA8DD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAE3G;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,OAAO,EACjB,WAAW,EAAE,mBAAmB,EAChC,MAAM,EAAE,CAAC,OAAO,EAAE,0BAA0B,GAAG,SAAS,KAAK,0BAA0B,GACtF,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,QAAQ,GAAG,yBAAyB,CAK7F;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,WAAW,EAAE,QAAQ,GAAG,GAAG,CAAC,mBAAmB,EAAE,0BAA0B,CAAC,CAuBzH"}
|
package/dist/metadata.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureMetadataSymbol, getOwnStandardConstructorMetadataBag } from '@fluojs/core/internal';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Defines the transform function type.
|
|
@@ -14,11 +14,11 @@ import { getOwnStandardConstructorMetadataBag, metadataSymbol } from '@fluojs/co
|
|
|
14
14
|
|
|
15
15
|
const standardSerializationClassMetadataKey = Symbol.for('fluo.standard.serialization.class');
|
|
16
16
|
const standardSerializationFieldMetadataKey = Symbol.for('fluo.standard.serialization.field');
|
|
17
|
+
ensureMetadataSymbol();
|
|
17
18
|
function getStandardMetadataBag(metadata) {
|
|
18
19
|
if (metadata === null || metadata === undefined) {
|
|
19
20
|
throw new Error('Decorator metadata is not available. Ensure your environment supports TC39 decorator metadata (Stage 3).');
|
|
20
21
|
}
|
|
21
|
-
void metadataSymbol;
|
|
22
22
|
return metadata;
|
|
23
23
|
}
|
|
24
24
|
function getFieldMetadataMap(metadata) {
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"output",
|
|
10
10
|
"transform"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.5",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^1.0.0-beta.
|
|
40
|
-
"@fluojs/http": "^1.0.0-beta.
|
|
39
|
+
"@fluojs/core": "^1.0.0-beta.4",
|
|
40
|
+
"@fluojs/http": "^1.0.0-beta.10"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^3.2.4"
|