@fluojs/serialization 1.0.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,151 @@
1
+ # @fluojs/serialization
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ fluo를 위한 클래스 기반 응답 직렬화 및 데코레이터 인지형 재귀 출력 가공 엔진입니다.
6
+
7
+ `@fluojs/serialization`은 애플리케이션의 **출력 경계(Output Boundary)**를 담당합니다. 내부 클래스 인스턴스나 복잡한 객체 그래프를 데코레이터 규칙이 반영된 일반 응답 형태로 변환하는 선언적인 방법을 제공합니다. 이를 통해 API 응답에 의도한 데이터만 노출되도록 보장합니다.
8
+
9
+ ## 목차
10
+
11
+ - [설치](#설치)
12
+ - [사용 시점](#사용-시점)
13
+ - [빠른 시작](#빠른-시작)
14
+ - [주요 패턴](#주요-패턴)
15
+ - [민감한 데이터 제외](#민감한-데이터-제외)
16
+ - [값 변환 (Transforming)](#값-변환-transforming)
17
+ - [순환 참조 처리](#순환-참조-처리)
18
+ - [HTTP 인터셉터와 함께 사용](#http-인터셉터와-함께-사용)
19
+ - [공개 API 개요](#공개-api-개요)
20
+ - [관련 패키지](#관련-패키지)
21
+ - [예제 소스](#예제-소스)
22
+
23
+ ## 설치
24
+
25
+ ```bash
26
+ pnpm add @fluojs/serialization
27
+ ```
28
+
29
+ ## 사용 시점
30
+
31
+ - JSON 응답에 포함될 클래스 속성을 정밀하게 제어하고 싶을 때.
32
+ - 비밀번호나 내부 ID와 같은 민감한 필드를 출력에서 숨겨야 할 때.
33
+ - 직렬화 과정에서 속성 값을 변환해야 할 때 (예: 날짜 형식 지정, 내부 열거형 매핑).
34
+ - 무한 루프를 유발할 수 있는 복잡한 객체 그래프를 안전하게 직렬화해야 할 때.
35
+
36
+ ## 빠른 시작
37
+
38
+ DTO나 엔티티 클래스에 데코레이터를 적용하고 `serialize` 함수 또는 `SerializerInterceptor`를 사용합니다.
39
+
40
+ ```typescript
41
+ import { Expose, Exclude, Transform, serialize } from '@fluojs/serialization';
42
+
43
+ class UserEntity {
44
+ @Expose()
45
+ id: string = '';
46
+
47
+ @Expose()
48
+ @Transform((val) => val.toUpperCase())
49
+ username: string = '';
50
+
51
+ @Exclude()
52
+ passwordHash: string = '';
53
+
54
+ constructor(partial: Partial<UserEntity>) {
55
+ Object.assign(this, partial);
56
+ }
57
+ }
58
+
59
+ const user = new UserEntity({ id: '1', username: 'fluo', passwordHash: 'secret' });
60
+ const result = serialize(user);
61
+
62
+ console.log(result);
63
+ // 출력: { id: "1", username: "FLUO" }
64
+ // passwordHash는 제외됩니다.
65
+ ```
66
+
67
+ ## 주요 패턴
68
+
69
+ ### 민감한 데이터 제외
70
+
71
+ `@Exclude()`를 사용하여 특정 속성이 출력에 절대 나타나지 않도록 합니다. 클래스 레벨에서 `@Expose({ excludeExtraneous: true })`를 사용하면 명시적으로 허용된 필드만 포함하는 "화이트리스트" 전략을 구현할 수 있습니다.
72
+
73
+ ```typescript
74
+ import { Expose, Exclude } from '@fluojs/serialization';
75
+
76
+ @Expose({ excludeExtraneous: true })
77
+ class SecureDto {
78
+ @Expose()
79
+ publicData: string = 'visible';
80
+
81
+ internalData: string = 'hidden'; // excludeExtraneous가 true이므로 숨겨짐
82
+ }
83
+ ```
84
+
85
+ ### 값 변환 (Transforming)
86
+
87
+ `@Transform()`을 사용하여 직렬화 중에 값을 수정합니다. 변환 함수는 현재 값을 인자로 받아 새로운 값을 반환해야 합니다.
88
+
89
+ ```typescript
90
+ import { Transform } from '@fluojs/serialization';
91
+
92
+ class ProductDto {
93
+ @Transform((price) => `$${price.toFixed(2)}`)
94
+ price: number = 0;
95
+ }
96
+ ```
97
+
98
+ ### 순환 참조 처리
99
+
100
+ fluo의 직렬화 엔진은 순환 참조를 자동으로 감지하고, 반복되는 참조에 대해 `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`, `bigint`, 함수, `symbol` 같은 값은 `@Transform(...)`이나 최종 HTTP 응답 작성 전에 직접 정규화하지 않으면 그대로 통과할 수 있습니다.
113
+
114
+ ### HTTP 인터셉터와 함께 사용
115
+
116
+ fluo HTTP 애플리케이션에서는 `SerializerInterceptor`를 사용하여 컨트롤러에서 나가는 모든 응답을 자동으로 직렬화할 수 있습니다.
117
+
118
+ ```typescript
119
+ import { Controller, Get, UseInterceptors } from '@fluojs/http';
120
+ import { SerializerInterceptor } from '@fluojs/serialization';
121
+
122
+ @Controller('/users')
123
+ @UseInterceptors(SerializerInterceptor)
124
+ class UsersController {
125
+ @Get('/')
126
+ findAll() {
127
+ return [new UserEntity({ ... }), new UserEntity({ ... })];
128
+ }
129
+ }
130
+ ```
131
+
132
+ ## 공개 API 개요
133
+
134
+ ### 데코레이터
135
+ - `@Expose(options?)`: 포함할 속성을 표시합니다. 클래스에 사용하여 기본 동작을 설정할 수도 있습니다.
136
+ - `@Exclude()`: 직렬화 중에 무시할 속성을 표시합니다.
137
+ - `@Transform(fn)`: 속성에 대한 변환 함수를 등록합니다.
138
+
139
+ ### 엔진
140
+ - `serialize(value)`: 객체/배열을 재귀적으로 순회하며 직렬화 규칙과 데코레이터를 적용합니다.
141
+ - `SerializerInterceptor`: 핸들러의 반환 값에 대해 `serialize`를 호출하는 fluo HTTP 인터셉터입니다.
142
+
143
+ ## 관련 패키지
144
+
145
+ - `@fluojs/http`: `SerializerInterceptor`를 통한 자동 출력 가공을 지원합니다.
146
+ - `@fluojs/validation`: **입력** 측면(일반 객체를 클래스 인스턴스로 변환)을 담당하는 대응 패키지입니다.
147
+
148
+ ## 예제 소스
149
+
150
+ - `packages/serialization/src/serialize.test.ts`: 다양한 직렬화 시나리오에 대한 상세 예제.
151
+ - `packages/serialization/src/serializer-interceptor.test.ts`: HTTP 컨텍스트 내에서의 사용법.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @fluojs/serialization
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Class-based response serialization and output shaping for fluo with decorator-aware recursive object walking.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Public API Overview](#public-api-overview)
14
+ - [Related Packages](#related-packages)
15
+ - [Example Sources](#example-sources)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add @fluojs/serialization
21
+ ```
22
+
23
+ ## When to Use
24
+
25
+ - when you need output DTOs to expose only a controlled subset of fields
26
+ - when sensitive values such as password hashes or internal identifiers must never leave the response boundary
27
+ - when response data needs lightweight synchronous transforms during serialization
28
+ - when you want an HTTP interceptor to apply the same serialization rules automatically
29
+
30
+ ## Quick Start
31
+
32
+ ```ts
33
+ import { Exclude, Expose, Transform, serialize } from '@fluojs/serialization';
34
+
35
+ class UserEntity {
36
+ @Expose()
37
+ id = '';
38
+
39
+ @Expose()
40
+ @Transform((value) => value.toUpperCase())
41
+ username = '';
42
+
43
+ @Exclude()
44
+ passwordHash = '';
45
+ }
46
+
47
+ const user = Object.assign(new UserEntity(), {
48
+ id: '1',
49
+ username: 'fluo',
50
+ passwordHash: 'secret',
51
+ });
52
+
53
+ console.log(serialize(user));
54
+ // { id: '1', username: 'FLUO' }
55
+ ```
56
+
57
+ ## Common Patterns
58
+
59
+ ### Expose-only output DTOs
60
+
61
+ ```ts
62
+ import { Expose } from '@fluojs/serialization';
63
+
64
+ @Expose({ excludeExtraneous: true })
65
+ class SecureDto {
66
+ @Expose()
67
+ publicData = 'visible';
68
+
69
+ internalData = 'hidden';
70
+ }
71
+ ```
72
+
73
+ ### Value transforms
74
+
75
+ ```ts
76
+ import { Transform } from '@fluojs/serialization';
77
+
78
+ class ProductDto {
79
+ @Transform((price) => `$${price.toFixed(2)}`)
80
+ price = 0;
81
+ }
82
+ ```
83
+
84
+ ### HTTP response shaping with an interceptor
85
+
86
+ ```ts
87
+ import { Controller, Get, UseInterceptors } from '@fluojs/http';
88
+ import { SerializerInterceptor } from '@fluojs/serialization';
89
+
90
+ @Controller('/users')
91
+ @UseInterceptors(SerializerInterceptor)
92
+ class UsersController {
93
+ @Get('/')
94
+ findAll() {
95
+ return [new UserEntity()];
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### Cycle-safe serialization
101
+
102
+ The serializer cuts cyclic references safely instead of recursing forever, so complex object graphs can still be turned into plain response-shaped objects without unbounded recursion.
103
+
104
+ ### Inherited decorator contracts
105
+
106
+ 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.
107
+
108
+ ### Plain-object safety
109
+
110
+ `serialize()` treats plain objects and null-prototype records as data containers, not decorated class instances. Objects with custom or unsafe `constructor` fields are walked safely without throwing.
111
+
112
+ ### Non-JSON leaf values
113
+
114
+ `serialize()` applies decorator metadata and recursively walks arrays/plain objects, but it does not coerce every leaf into strict JSON types. Values such as `Date`, `bigint`, functions, and symbols can pass through unchanged unless you normalize them with `@Transform(...)` or before writing the final HTTP response.
115
+
116
+ ## Public API Overview
117
+
118
+ - **Decorators**: `Expose`, `Exclude`, `Transform`
119
+ - **Engine**: `serialize(value)`
120
+ - **HTTP integration**: `SerializerInterceptor`
121
+
122
+ ## Related Packages
123
+
124
+ - `@fluojs/http`: applies `SerializerInterceptor` to HTTP handlers
125
+ - `@fluojs/validation`: handles input-side DTO materialization and validation
126
+
127
+ ## Example Sources
128
+
129
+ - `packages/serialization/src/serialize.test.ts`
130
+ - `packages/serialization/src/serializer-interceptor.test.ts`
@@ -0,0 +1,18 @@
1
+ type StandardFieldDecoratorFn = <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>) => void;
2
+ type FieldDecoratorLike = StandardFieldDecoratorFn;
3
+ /**
4
+ * Excludes the decorated field from serialized output.
5
+ *
6
+ * @returns A field decorator that marks the property as omitted during serialization.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * class UserEntity {
11
+ * @Exclude()
12
+ * passwordHash = '';
13
+ * }
14
+ * ```
15
+ */
16
+ export declare function Exclude(): FieldDecoratorLike;
17
+ export {};
18
+ //# sourceMappingURL=exclude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exclude.d.ts","sourceRoot":"","sources":["../../src/decorators/exclude.ts"],"names":[],"mappings":"AAIA,KAAK,wBAAwB,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AAC1H,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,IAAI,kBAAkB,CAS5C"}
@@ -0,0 +1,23 @@
1
+ import { updateFieldSerializationMetadata } from '../metadata.js';
2
+ /**
3
+ * Excludes the decorated field from serialized output.
4
+ *
5
+ * @returns A field decorator that marks the property as omitted during serialization.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * class UserEntity {
10
+ * @Exclude()
11
+ * passwordHash = '';
12
+ * }
13
+ * ```
14
+ */
15
+ export function Exclude() {
16
+ const decorator = (_value, context) => {
17
+ updateFieldSerializationMetadata(context.metadata, context.name, current => ({
18
+ ...current,
19
+ excluded: true
20
+ }));
21
+ };
22
+ return decorator;
23
+ }
@@ -0,0 +1,33 @@
1
+ type StandardClassDecoratorFn = (value: Function, context: ClassDecoratorContext) => void;
2
+ type StandardFieldDecoratorFn = <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>) => void;
3
+ type ClassOrFieldDecoratorLike = StandardClassDecoratorFn & StandardFieldDecoratorFn;
4
+ /**
5
+ * Class-level options accepted by `@Expose(...)`.
6
+ */
7
+ export interface ExposeClassOptions {
8
+ /**
9
+ * When enabled on a class, only fields marked with `@Expose()` are emitted.
10
+ */
11
+ excludeExtraneous?: boolean;
12
+ }
13
+ /**
14
+ * Marks a class or field as serializable output.
15
+ *
16
+ * - On classes, configures class-level serialization behavior.
17
+ * - On fields, marks the field as explicitly exposed.
18
+ *
19
+ * @param options Optional class-level serialization settings.
20
+ * @returns A decorator that updates serialization metadata on the class or field.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * @Expose({ excludeExtraneous: true })
25
+ * class UserDto {
26
+ * @Expose()
27
+ * id = '';
28
+ * }
29
+ * ```
30
+ */
31
+ export declare function Expose(options?: ExposeClassOptions): ClassOrFieldDecoratorLike;
32
+ export {};
33
+ //# sourceMappingURL=expose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expose.d.ts","sourceRoot":"","sources":["../../src/decorators/expose.ts"],"names":[],"mappings":"AAIA,KAAK,wBAAwB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAC1F,KAAK,wBAAwB,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AAG1H,KAAK,yBAAyB,GAAG,wBAAwB,GAAG,wBAAwB,CAAC;AAErF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,yBAAyB,CAmB9E"}
@@ -0,0 +1,39 @@
1
+ import { updateClassSerializationOptions, updateFieldSerializationMetadata } from '../metadata.js';
2
+
3
+ /**
4
+ * Class-level options accepted by `@Expose(...)`.
5
+ */
6
+
7
+ /**
8
+ * Marks a class or field as serializable output.
9
+ *
10
+ * - On classes, configures class-level serialization behavior.
11
+ * - On fields, marks the field as explicitly exposed.
12
+ *
13
+ * @param options Optional class-level serialization settings.
14
+ * @returns A decorator that updates serialization metadata on the class or field.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * @Expose({ excludeExtraneous: true })
19
+ * class UserDto {
20
+ * @Expose()
21
+ * id = '';
22
+ * }
23
+ * ```
24
+ */
25
+ export function Expose(options) {
26
+ const decorator = (_value, context) => {
27
+ if (context.kind === 'class') {
28
+ updateClassSerializationOptions(context.metadata, {
29
+ excludeExtraneous: options?.excludeExtraneous
30
+ });
31
+ return;
32
+ }
33
+ updateFieldSerializationMetadata(context.metadata, context.name, current => ({
34
+ ...current,
35
+ exposed: true
36
+ }));
37
+ };
38
+ return decorator;
39
+ }
@@ -0,0 +1,20 @@
1
+ import { type TransformFunction } from '../metadata.js';
2
+ type StandardFieldDecoratorFn = <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>) => void;
3
+ type FieldDecoratorLike = StandardFieldDecoratorFn;
4
+ /**
5
+ * Applies a synchronous transformation to the decorated field during serialization.
6
+ *
7
+ * @param transform Function that maps the raw field value to the serialized value.
8
+ * @returns A field decorator that appends the transform to the field metadata.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * class ProductDto {
13
+ * @Transform((price) => `$${Number(price).toFixed(2)}`)
14
+ * price = 0;
15
+ * }
16
+ * ```
17
+ */
18
+ export declare function Transform(transform: TransformFunction): FieldDecoratorLike;
19
+ export {};
20
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../src/decorators/transform.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAoC,MAAM,gBAAgB,CAAC;AAE1F,KAAK,wBAAwB,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AAC1H,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AAEnD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,iBAAiB,GAAG,kBAAkB,CAS1E"}
@@ -0,0 +1,24 @@
1
+ import { updateFieldSerializationMetadata } from '../metadata.js';
2
+ /**
3
+ * Applies a synchronous transformation to the decorated field during serialization.
4
+ *
5
+ * @param transform Function that maps the raw field value to the serialized value.
6
+ * @returns A field decorator that appends the transform to the field metadata.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * class ProductDto {
11
+ * @Transform((price) => `$${Number(price).toFixed(2)}`)
12
+ * price = 0;
13
+ * }
14
+ * ```
15
+ */
16
+ export function Transform(transform) {
17
+ const decorator = (_value, context) => {
18
+ updateFieldSerializationMetadata(context.metadata, context.name, current => ({
19
+ ...current,
20
+ transforms: [...(current?.transforms ?? []), transform]
21
+ }));
22
+ };
23
+ return decorator;
24
+ }
@@ -0,0 +1,6 @@
1
+ export * from './decorators/exclude.js';
2
+ export * from './decorators/expose.js';
3
+ export * from './decorators/transform.js';
4
+ export * from './serialize.js';
5
+ export * from './serializer-interceptor.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './decorators/exclude.js';
2
+ export * from './decorators/expose.js';
3
+ export * from './decorators/transform.js';
4
+ export * from './serialize.js';
5
+ export * from './serializer-interceptor.js';
@@ -0,0 +1,15 @@
1
+ import { type MetadataPropertyKey } from '@fluojs/core';
2
+ export type TransformFunction = (value: unknown) => unknown;
3
+ export interface ClassSerializationOptions {
4
+ excludeExtraneous?: boolean;
5
+ }
6
+ export interface SerializationFieldMetadata {
7
+ excluded?: boolean;
8
+ exposed?: boolean;
9
+ transforms?: TransformFunction[];
10
+ }
11
+ export declare function updateClassSerializationOptions(metadata: unknown, partial: ClassSerializationOptions): void;
12
+ export declare function updateFieldSerializationMetadata(metadata: unknown, propertyKey: MetadataPropertyKey, update: (current: SerializationFieldMetadata | undefined) => SerializationFieldMetadata): void;
13
+ export declare function getClassSerializationOptions(constructor: Function): ClassSerializationOptions;
14
+ export declare function getFieldSerializationMetadata(constructor: Function): Map<MetadataPropertyKey, SerializationFieldMetadata>;
15
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +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,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;AAE5D,MAAM,WAAW,yBAAyB;IACxC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAClC;AAiED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAE3G;AAED,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,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,QAAQ,GAAG,yBAAyB,CAK7F;AAED,wBAAgB,6BAA6B,CAAC,WAAW,EAAE,QAAQ,GAAG,GAAG,CAAC,mBAAmB,EAAE,0BAA0B,CAAC,CAuBzH"}
@@ -0,0 +1,79 @@
1
+ import { metadataSymbol } from '@fluojs/core/internal';
2
+ const standardSerializationClassMetadataKey = Symbol.for('fluo.standard.serialization.class');
3
+ const standardSerializationFieldMetadataKey = Symbol.for('fluo.standard.serialization.field');
4
+ function getStandardMetadataBag(metadata) {
5
+ if (metadata === null || metadata === undefined) {
6
+ throw new Error('Decorator metadata is not available. Ensure your environment supports TC39 decorator metadata (Stage 3).');
7
+ }
8
+ void metadataSymbol;
9
+ return metadata;
10
+ }
11
+ function getFieldMetadataMap(metadata) {
12
+ const bag = getStandardMetadataBag(metadata);
13
+ const current = bag[standardSerializationFieldMetadataKey];
14
+ if (current) {
15
+ return current;
16
+ }
17
+ const created = new Map();
18
+ bag[standardSerializationFieldMetadataKey] = created;
19
+ return created;
20
+ }
21
+ function getClassMetadataObject(metadata) {
22
+ const bag = getStandardMetadataBag(metadata);
23
+ const current = bag[standardSerializationClassMetadataKey];
24
+ if (current) {
25
+ return current;
26
+ }
27
+ const created = {};
28
+ bag[standardSerializationClassMetadataKey] = created;
29
+ return created;
30
+ }
31
+ function getOwnMetadataBagFromConstructor(constructor) {
32
+ if (!Object.prototype.hasOwnProperty.call(constructor, metadataSymbol)) {
33
+ return undefined;
34
+ }
35
+ return constructor[metadataSymbol];
36
+ }
37
+ function getConstructorMetadataBags(constructor) {
38
+ const bags = [];
39
+ let current = constructor;
40
+ while (current && current !== Function.prototype) {
41
+ const bag = getOwnMetadataBagFromConstructor(current);
42
+ if (bag) {
43
+ bags.unshift(bag);
44
+ }
45
+ current = Object.getPrototypeOf(current);
46
+ }
47
+ return bags;
48
+ }
49
+ export function updateClassSerializationOptions(metadata, partial) {
50
+ Object.assign(getClassMetadataObject(metadata), partial);
51
+ }
52
+ export function updateFieldSerializationMetadata(metadata, propertyKey, update) {
53
+ const map = getFieldMetadataMap(metadata);
54
+ map.set(propertyKey, update(map.get(propertyKey)));
55
+ }
56
+ export function getClassSerializationOptions(constructor) {
57
+ return getConstructorMetadataBags(constructor).reduce((options, bag) => ({
58
+ ...options,
59
+ ...bag[standardSerializationClassMetadataKey]
60
+ }), {});
61
+ }
62
+ export function getFieldSerializationMetadata(constructor) {
63
+ const merged = new Map();
64
+ for (const bag of getConstructorMetadataBags(constructor)) {
65
+ const fieldMetadata = bag[standardSerializationFieldMetadataKey];
66
+ if (!fieldMetadata) {
67
+ continue;
68
+ }
69
+ for (const [propertyKey, metadata] of fieldMetadata.entries()) {
70
+ const current = merged.get(propertyKey);
71
+ merged.set(propertyKey, {
72
+ ...current,
73
+ ...metadata,
74
+ transforms: [...(current?.transforms ?? []), ...(metadata.transforms ?? [])]
75
+ });
76
+ }
77
+ }
78
+ return merged;
79
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Serializes class instances and object graphs into JSON-safe plain values.
3
+ *
4
+ * Serialization honors `@Expose()`, `@Exclude()`, and `@Transform()` metadata.
5
+ * Cycles and repeated references are handled without unbounded recursion.
6
+ *
7
+ * @typeParam T Input value type.
8
+ * @param value Value or object graph to serialize.
9
+ * @returns A plain JSON-safe structure ready for HTTP response writing.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * class UserEntity {
14
+ * id = '1';
15
+ * }
16
+ *
17
+ * serialize(new UserEntity());
18
+ * ```
19
+ */
20
+ export declare function serialize<T = unknown>(value: T): unknown;
21
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAiPA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAOxD"}
@@ -0,0 +1,187 @@
1
+ import { getClassSerializationOptions, getFieldSerializationMetadata } from './metadata.js';
2
+ function isObjectLike(value) {
3
+ return typeof value === 'object' && value !== null;
4
+ }
5
+ function isPlainObject(value) {
6
+ if (!isObjectLike(value)) {
7
+ return false;
8
+ }
9
+ const prototype = Object.getPrototypeOf(value);
10
+ return prototype === Object.prototype || prototype === null;
11
+ }
12
+ function getSerializableConstructor(value) {
13
+ const prototype = Object.getPrototypeOf(value);
14
+ if (prototype === null || prototype === Object.prototype) {
15
+ return undefined;
16
+ }
17
+ const constructor = Reflect.get(prototype, 'constructor');
18
+ return typeof constructor === 'function' ? constructor : undefined;
19
+ }
20
+ function applyTransforms(value, metadata) {
21
+ let transformed = value;
22
+ for (const transform of metadata.transforms ?? []) {
23
+ transformed = transform(transformed);
24
+ }
25
+ return transformed;
26
+ }
27
+ function assignSerializedProperty(target, propertyKey, value) {
28
+ if (propertyKey === '__proto__' || propertyKey === 'constructor' || propertyKey === 'prototype') {
29
+ Object.defineProperty(target, propertyKey, {
30
+ configurable: true,
31
+ enumerable: true,
32
+ value,
33
+ writable: true
34
+ });
35
+ return;
36
+ }
37
+ target[propertyKey] = value;
38
+ }
39
+ function resolveCandidateKeys(value, fieldMetadata, excludeExtraneous) {
40
+ if (excludeExtraneous) {
41
+ return [...fieldMetadata.entries()].filter(([, metadata]) => metadata.exposed === true).map(([propertyKey]) => propertyKey);
42
+ }
43
+ const keys = new Set([...Object.keys(value), ...Object.getOwnPropertySymbols(value)]);
44
+ for (const [propertyKey, metadata] of fieldMetadata) {
45
+ if (metadata.exposed === true) {
46
+ keys.add(propertyKey);
47
+ }
48
+ }
49
+ return [...keys];
50
+ }
51
+ function getCachedMetadata(constructor, context) {
52
+ const cached = context.metadataCache.get(constructor);
53
+ if (cached) {
54
+ return cached;
55
+ }
56
+ const next = {
57
+ classOptions: getClassSerializationOptions(constructor),
58
+ fieldMetadata: getFieldSerializationMetadata(constructor)
59
+ };
60
+ context.metadataCache.set(constructor, next);
61
+ return next;
62
+ }
63
+ function getCircularOrSharedValue(value, context) {
64
+ const cached = context.references.get(value);
65
+ if (!cached) {
66
+ return undefined;
67
+ }
68
+ if (cached.active) {
69
+ return undefined;
70
+ }
71
+ return cached.value;
72
+ }
73
+ function markSerializationStart(value, serialized, context) {
74
+ context.references.set(value, {
75
+ active: true,
76
+ value: serialized
77
+ });
78
+ }
79
+ function markSerializationComplete(value, context) {
80
+ const cached = context.references.get(value);
81
+ if (!cached) {
82
+ return;
83
+ }
84
+ cached.active = false;
85
+ }
86
+ function serializeWithTrackedReference(value, context, create, fill) {
87
+ const cachedValue = getCircularOrSharedValue(value, context);
88
+ if (context.references.has(value)) {
89
+ return cachedValue;
90
+ }
91
+ const serialized = create();
92
+ markSerializationStart(value, serialized, context);
93
+ try {
94
+ fill(serialized);
95
+ return serialized;
96
+ } finally {
97
+ markSerializationComplete(value, context);
98
+ }
99
+ }
100
+ function serializeClassInstance(value, context) {
101
+ const constructor = getSerializableConstructor(value);
102
+ if (!constructor) {
103
+ return serializeRecord(value, context);
104
+ }
105
+ const {
106
+ classOptions,
107
+ fieldMetadata
108
+ } = getCachedMetadata(constructor, context);
109
+ const hasMetadata = fieldMetadata.size > 0 || classOptions.excludeExtraneous === true;
110
+ if (!hasMetadata) {
111
+ return serializeRecord(value, context);
112
+ }
113
+ return serializeWithTrackedReference(value, context, () => ({}), serialized => {
114
+ const candidateKeys = resolveCandidateKeys(value, fieldMetadata, classOptions.excludeExtraneous === true);
115
+ for (const propertyKey of candidateKeys) {
116
+ const metadata = fieldMetadata.get(propertyKey);
117
+ if (metadata?.excluded) {
118
+ continue;
119
+ }
120
+ const raw = value[propertyKey];
121
+ if (raw === undefined && classOptions.excludeExtraneous === true && metadata?.exposed !== true) {
122
+ continue;
123
+ }
124
+ const transformed = metadata ? applyTransforms(raw, metadata) : raw;
125
+ assignSerializedProperty(serialized, propertyKey, serializeInternal(transformed, context));
126
+ }
127
+ });
128
+ }
129
+ function serializeRecord(value, context) {
130
+ const symbolKeys = Object.getOwnPropertySymbols(value).filter(key => Object.prototype.propertyIsEnumerable.call(value, key));
131
+ const keys = [...Object.keys(value), ...symbolKeys];
132
+ return serializeWithTrackedReference(value, context, () => ({}), serialized => {
133
+ for (const propertyKey of keys) {
134
+ const propertyValue = value[propertyKey];
135
+ assignSerializedProperty(serialized, propertyKey, serializeInternal(propertyValue, context));
136
+ }
137
+ });
138
+ }
139
+ function serializeInternal(value, context) {
140
+ if (value === null || value === undefined) {
141
+ return value;
142
+ }
143
+ if (Array.isArray(value)) {
144
+ return serializeWithTrackedReference(value, context, () => [], serialized => {
145
+ for (const item of value) {
146
+ serialized.push(serializeInternal(item, context));
147
+ }
148
+ });
149
+ }
150
+ if (value instanceof Date) {
151
+ return value;
152
+ }
153
+ if (isObjectLike(value)) {
154
+ if (isPlainObject(value)) {
155
+ return serializeRecord(value, context);
156
+ }
157
+ return serializeClassInstance(value, context);
158
+ }
159
+ return value;
160
+ }
161
+
162
+ /**
163
+ * Serializes class instances and object graphs into JSON-safe plain values.
164
+ *
165
+ * Serialization honors `@Expose()`, `@Exclude()`, and `@Transform()` metadata.
166
+ * Cycles and repeated references are handled without unbounded recursion.
167
+ *
168
+ * @typeParam T Input value type.
169
+ * @param value Value or object graph to serialize.
170
+ * @returns A plain JSON-safe structure ready for HTTP response writing.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * class UserEntity {
175
+ * id = '1';
176
+ * }
177
+ *
178
+ * serialize(new UserEntity());
179
+ * ```
180
+ */
181
+ export function serialize(value) {
182
+ const context = {
183
+ metadataCache: new WeakMap(),
184
+ references: new WeakMap()
185
+ };
186
+ return serializeInternal(value, context);
187
+ }
@@ -0,0 +1,13 @@
1
+ import type { CallHandler, Interceptor, InterceptorContext } from '@fluojs/http';
2
+ /**
3
+ * HTTP interceptor that serializes handler results before response writing.
4
+ *
5
+ * @remarks
6
+ * Use this at the controller or route level when handlers return class instances
7
+ * and you want `@Expose()`, `@Exclude()`, and `@Transform()` metadata applied
8
+ * automatically.
9
+ */
10
+ export declare class SerializerInterceptor implements Interceptor {
11
+ intercept(_context: InterceptorContext, next: CallHandler): Promise<unknown>;
12
+ }
13
+ //# sourceMappingURL=serializer-interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer-interceptor.d.ts","sourceRoot":"","sources":["../src/serializer-interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIjF;;;;;;;GAOG;AACH,qBAAa,qBAAsB,YAAW,WAAW;IACjD,SAAS,CAAC,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;CAInF"}
@@ -0,0 +1,16 @@
1
+ import { serialize } from './serialize.js';
2
+
3
+ /**
4
+ * HTTP interceptor that serializes handler results before response writing.
5
+ *
6
+ * @remarks
7
+ * Use this at the controller or route level when handlers return class instances
8
+ * and you want `@Expose()`, `@Exclude()`, and `@Transform()` metadata applied
9
+ * automatically.
10
+ */
11
+ export class SerializerInterceptor {
12
+ async intercept(_context, next) {
13
+ const value = await next.handle();
14
+ return serialize(value);
15
+ }
16
+ }
File without changes
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@fluojs/serialization",
3
+ "description": "Class-based response serialization and output shaping interceptors for Fluo.",
4
+ "keywords": [
5
+ "fluo",
6
+ "serialization",
7
+ "response",
8
+ "interceptor",
9
+ "output",
10
+ "transform"
11
+ ],
12
+ "version": "1.0.0-beta.1",
13
+ "private": false,
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/fluojs/fluo.git",
18
+ "directory": "packages/serialization"
19
+ },
20
+ "engines": {
21
+ "node": ">=20.0.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "dependencies": {
39
+ "@fluojs/core": "^1.0.0-beta.1",
40
+ "@fluojs/http": "^1.0.0-beta.1"
41
+ },
42
+ "devDependencies": {
43
+ "vitest": "^3.2.4"
44
+ },
45
+ "scripts": {
46
+ "prebuild": "node ../../tooling/scripts/clean-dist.mjs",
47
+ "build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
48
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
49
+ "test": "pnpm exec vitest run -c vitest.config.ts",
50
+ "test:watch": "pnpm exec vitest -c vitest.config.ts"
51
+ }
52
+ }