@fluojs/serialization 1.0.3 → 1.0.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 CHANGED
@@ -70,6 +70,8 @@ class SecureDto {
70
70
  }
71
71
  ```
72
72
 
73
+ `ExposeClassOptions`는 `Expose(...)`가 받는 class-level option을 표현하는 export 타입입니다. DTO가 field-level `@Expose()` metadata가 있는 field만 내보내야 할 때 `excludeExtraneous: true`를 사용하세요.
74
+
73
75
  ### 값 변환
74
76
 
75
77
  ```ts
@@ -86,7 +88,7 @@ class ProductDto {
86
88
  ### HTTP 인터셉터와 함께 사용
87
89
 
88
90
  ```ts
89
- import { Controller, Get, UseInterceptors } from '@fluojs/http';
91
+ import { Controller, Get, type RequestContext, UseInterceptors } from '@fluojs/http';
90
92
  import { SerializerInterceptor } from '@fluojs/serialization';
91
93
 
92
94
  @Controller('/users')
@@ -96,10 +98,21 @@ class UsersController {
96
98
  findAll() {
97
99
  return [new UserEntity()];
98
100
  }
101
+
102
+ @Get('/export.csv')
103
+ async exportCsv(_input: undefined, context: RequestContext) {
104
+ context.response.setHeader('Content-Type', 'text/csv; charset=utf-8');
105
+ await context.response.send('id,username\n1,fluo');
106
+ }
99
107
  }
100
108
  ```
101
109
 
102
- `SerializerInterceptor`는 일반 HTTP 응답 writer가 아직 소유한 값만 직렬화합니다. 핸들러나 응답 헬퍼가 SSE 스트림처럼 `RequestContext.response`를 직접 커밋한 경우, 인터셉터는 해당 핸들러 소유 값을 그대로 반환하여 request pipeline의 응답 소유권을 보존합니다.
110
+ route는 서로 다른 응답 소유자를 사용합니다.
111
+
112
+ - **Framework-managed response**: `findAll()`은 `RequestContext.response`가 아직 commit되지 않은 상태에서 반환합니다. `SerializerInterceptor`가 반환된 DTO를 직렬화한 뒤 runtime response writer가 결과를 commit합니다.
113
+ - **Handler-owned response**: `exportCsv()`는 최종 payload를 `RequestContext.response.send(...)`로 직접 씁니다. `send(...)`, `redirect(...)`, 또는 수동 streaming helper가 response를 commit하면 `SerializerInterceptor`는 `serialize(...)`를 건너뛰고 `next.handle()`에서 받은 값을 그대로 반환합니다. 이 보장은 `SerializerInterceptor`에만 해당하며, 다른 interceptor는 chain 결과를 계속 변환할 수 있습니다. 이와 별개로 dispatcher는 commit된 response를 확인하고 두 번째 success-response write를 건너뜁니다.
114
+
115
+ 직접 쓰는 payload는 최종 결과로 취급하세요. 필요한 field filtering이나 encoding을 commit 전에 적용해야 합니다. Handler/runtime response ownership이 commit된 뒤에는 serialization이 응답을 후처리할 수 없습니다.
103
116
 
104
117
  ### 순환 참조 처리
105
118
 
@@ -125,7 +138,8 @@ Decorated metadata가 없는 class instance도 재귀적으로 순회하므로,
125
138
 
126
139
  - **데코레이터**: `Expose`, `Exclude`, `Transform`
127
140
  - **엔진**: `serialize(value)`는 class instance, 배열, plain object, mixed graph를 재귀적으로 순회하며, 직접 transform하지 않은 opaque built-in 및 non-JSON leaf 값은 보존합니다.
128
- - **HTTP 통합**: `SerializerInterceptor`는 아직 commit되지 않은 handler 결과를 직렬화하고, response이미 commit된 뒤에는 handler 소유 값을 그대로 반환합니다.
141
+ - **HTTP 통합**: `SerializerInterceptor`는 아직 commit되지 않은 handler 결과를 직렬화합니다. Response가 commit된 뒤에는 `next.handle()`에서 받은 값을 그대로 반환하지만, 다른 interceptor는 chain 결과를 계속 변환할 수 있습니다.
142
+ - **타입**: `ExposeClassOptions`는 class-level `Expose(...)` option 타입으로 root entrypoint에서 export되며, `TransformFunction`은 `Transform(...)`에 전달하는 callback 타입으로 export됩니다.
129
143
 
130
144
  `Expose`는 class와 field에 적용할 수 있습니다. `Exclude`와 `Transform`은 field에 적용합니다.
131
145
 
package/README.md CHANGED
@@ -70,6 +70,8 @@ class SecureDto {
70
70
  }
71
71
  ```
72
72
 
73
+ `ExposeClassOptions` is the exported class-level options type accepted by `Expose(...)`. Use `excludeExtraneous: true` when a DTO should emit only fields with field-level `@Expose()` metadata.
74
+
73
75
  ### Value transforms
74
76
 
75
77
  ```ts
@@ -86,7 +88,7 @@ When the same field is decorated in a base class and a derived class, transforms
86
88
  ### HTTP response shaping with an interceptor
87
89
 
88
90
  ```ts
89
- import { Controller, Get, UseInterceptors } from '@fluojs/http';
91
+ import { Controller, Get, type RequestContext, UseInterceptors } from '@fluojs/http';
90
92
  import { SerializerInterceptor } from '@fluojs/serialization';
91
93
 
92
94
  @Controller('/users')
@@ -96,10 +98,21 @@ class UsersController {
96
98
  findAll() {
97
99
  return [new UserEntity()];
98
100
  }
101
+
102
+ @Get('/export.csv')
103
+ async exportCsv(_input: undefined, context: RequestContext) {
104
+ context.response.setHeader('Content-Type', 'text/csv; charset=utf-8');
105
+ await context.response.send('id,username\n1,fluo');
106
+ }
99
107
  }
100
108
  ```
101
109
 
102
- `SerializerInterceptor` only serializes values that still belong to the normal HTTP response writer. If a handler or response helper commits `RequestContext.response` directly, such as an SSE stream, the interceptor returns that handler-owned value unchanged so the request pipeline preserves response ownership.
110
+ The two routes use different response owners:
111
+
112
+ - **Framework-managed response**: `findAll()` returns while `RequestContext.response` is still uncommitted. `SerializerInterceptor` serializes the returned DTOs, then the runtime response writer commits the result.
113
+ - **Handler-owned response**: `exportCsv()` writes the final payload through `RequestContext.response.send(...)`. Once `send(...)`, `redirect(...)`, or a manual streaming helper commits the response, `SerializerInterceptor` bypasses `serialize(...)` and returns the value it received from `next.handle()` unchanged. This guarantee is specific to `SerializerInterceptor`; other interceptors may still transform the chain result. Independently, the dispatcher sees the committed response and skips a second success-response write.
114
+
115
+ Treat a directly written payload as final: apply any required field filtering or encoding before the commit. Serialization cannot post-process a response after handler/runtime response ownership has been committed.
103
116
 
104
117
  ### Cycle-safe serialization
105
118
 
@@ -125,7 +138,8 @@ Undecorated class instances are still traversed recursively, so decorated nested
125
138
 
126
139
  - **Decorators**: `Expose`, `Exclude`, `Transform`
127
140
  - **Engine**: `serialize(value)` recursively walks class instances, arrays, plain objects, and mixed graphs while preserving opaque built-ins and non-JSON leaf values unless you transform them
128
- - **HTTP integration**: `SerializerInterceptor` serializes uncommitted handler results and returns handler-owned values unchanged after the response has already been committed
141
+ - **HTTP integration**: `SerializerInterceptor` serializes uncommitted handler results; after the response is committed, it returns the value it received from `next.handle()` unchanged, although other interceptors may still transform the chain result
142
+ - **Types**: `ExposeClassOptions` is exported from the root entrypoint for class-level `Expose(...)` options, and `TransformFunction` is exported for callbacks passed to `Transform(...)`
129
143
 
130
144
  `Expose` can be applied to classes and fields. `Exclude` and `Transform` apply to fields.
131
145
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './decorators/exclude.js';
2
2
  export * from './decorators/expose.js';
3
3
  export * from './decorators/transform.js';
4
+ export type { TransformFunction } from './metadata.js';
4
5
  export * from './serialize.js';
5
6
  export * from './serializer-interceptor.js';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export * from './decorators/exclude.js';
2
2
  export * from './decorators/expose.js';
3
3
  export * from './decorators/transform.js';
4
4
  export * from './serialize.js';
5
- export * from './serializer-interceptor.js';
5
+ export * from './serializer-interceptor.js';
6
+ export {};
@@ -1,4 +1,4 @@
1
- import { type MetadataPropertyKey } from '@fluojs/core';
1
+ import type { MetadataPropertyKey } from '@fluojs/core';
2
2
  /**
3
3
  * Defines the transform function type.
4
4
  */
@@ -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;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"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,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;AA4DD;;;;;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,CAQ7F;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 { ensureMetadataSymbol, getOwnStandardConstructorMetadataBag } from '@fluojs/core/internal';
1
+ import { getOwnConstructorRequestPipelineMetadataBag } from '@fluojs/core/request-pipeline';
2
2
 
3
3
  /**
4
4
  * Defines the transform function type.
@@ -14,7 +14,6 @@ import { ensureMetadataSymbol, getOwnStandardConstructorMetadataBag } from '@flu
14
14
 
15
15
  const standardSerializationClassMetadataKey = Symbol.for('fluo.standard.serialization.class');
16
16
  const standardSerializationFieldMetadataKey = Symbol.for('fluo.standard.serialization.field');
17
- ensureMetadataSymbol();
18
17
  function getStandardMetadataBag(metadata) {
19
18
  if (metadata === null || metadata === undefined) {
20
19
  throw new Error('Decorator metadata is not available. Ensure your environment supports TC39 decorator metadata (Stage 3).');
@@ -42,7 +41,7 @@ function getClassMetadataObject(metadata) {
42
41
  return created;
43
42
  }
44
43
  function getOwnMetadataBagFromConstructor(constructor) {
45
- return getOwnStandardConstructorMetadataBag(constructor);
44
+ return getOwnConstructorRequestPipelineMetadataBag(constructor);
46
45
  }
47
46
  function getConstructorMetadataBags(constructor) {
48
47
  const bags = [];
@@ -86,10 +85,11 @@ export function updateFieldSerializationMetadata(metadata, propertyKey, update)
86
85
  * @returns The get class serialization options result.
87
86
  */
88
87
  export function getClassSerializationOptions(constructor) {
89
- return getConstructorMetadataBags(constructor).reduce((options, bag) => ({
90
- ...options,
91
- ...bag[standardSerializationClassMetadataKey]
92
- }), {});
88
+ const options = {};
89
+ for (const bag of getConstructorMetadataBags(constructor)) {
90
+ Object.assign(options, bag[standardSerializationClassMetadataKey]);
91
+ }
92
+ return options;
93
93
  }
94
94
 
95
95
  /**
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "output",
10
10
  "transform"
11
11
  ],
12
- "version": "1.0.3",
12
+ "version": "1.0.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.3",
40
- "@fluojs/http": "^1.1.0"
39
+ "@fluojs/core": "^1.1.0",
40
+ "@fluojs/http": "^2.0.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^3.2.4"