@zudojs/serialization 1.1.0 → 1.2.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.md +25 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/serializerEnvelope/envelope.core.d.ts +6 -2
- package/dist/serializerEnvelope/envelope.core.js +15 -10
- package/dist/serializerJson/index.d.ts +1 -1
- package/dist/serializerJson/index.js +1 -1
- package/dist/serializerJson/jsonSerializer.core.d.ts +21 -5
- package/dist/serializerJson/jsonSerializer.core.js +17 -16
- package/dist/serializerJson/jsonSerializer.restore.d.ts +2 -2
- package/dist/serializerJson/jsonSerializer.restore.js +18 -2
- package/dist/serializerJson/jsonSerializer.transform.d.ts +2 -2
- package/dist/serializerJson/jsonSerializer.transform.js +7 -6
- package/dist/serializerRegistry/serializerRegistry.factory.d.ts +6 -1
- package/dist/serializerRegistry/serializerRegistry.factory.js +6 -2
- package/dist/serializerTransformPolicy/index.d.ts +10 -0
- package/dist/serializerTransformPolicy/index.js +10 -0
- package/dist/serializerTransformPolicy/transformerLookup.core.d.ts +31 -0
- package/dist/serializerTransformPolicy/transformerLookup.core.js +37 -0
- package/dist/serializerTransformPolicy/transformerOutput.helper.d.ts +24 -0
- package/dist/serializerTransformPolicy/transformerOutput.helper.js +58 -0
- package/dist/serializerTransforms/transformerRegistry.core.d.ts +5 -1
- package/dist/serializerTransforms/transformerRegistry.core.js +7 -3
- package/dist/serializerTypes/serializer.type.d.ts +22 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -70,13 +70,37 @@ const registry = new TransformerRegistry();
|
|
|
70
70
|
registry.register({
|
|
71
71
|
type: "Money",
|
|
72
72
|
canSerialize: (v): v is Money => v instanceof Money,
|
|
73
|
-
serialize: (v) => ({ $type: "Money", $value:
|
|
73
|
+
serialize: (v) => (v as Money).toString(), // wrapped as { $type: "Money", $value: "..." }
|
|
74
74
|
deserialize: (v) => Money.parse((v as { $value: string }).$value),
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
const serializer = new JSONSerializer({ transformers: registry });
|
|
78
|
+
// Money uses your transformer; Date, BigInt, Map, Set, Buffer and Error still
|
|
79
|
+
// use the built-ins.
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
Your registry is consulted first, so registering a tag the built-ins use (say
|
|
83
|
+
`"Date"`) replaces that built-in. Transformers you add to the registry later are
|
|
84
|
+
picked up.
|
|
85
|
+
|
|
86
|
+
**The transformer contract.** On the wire a transformed value is always
|
|
87
|
+
`{ "$type": <type>, "$value": ... }`.
|
|
88
|
+
|
|
89
|
+
- `serialize` returns either just the value to store, which the serializer
|
|
90
|
+
wraps as `{ $type: type, $value: <returned> }`, or that full tagged object
|
|
91
|
+
itself (as the built-ins do). A plain object with its own string `$type` is
|
|
92
|
+
taken as the full tagged object; anything else is wrapped. Special values
|
|
93
|
+
nested inside what you return (a `Date`, a `BigInt`) are transformed too.
|
|
94
|
+
- `deserialize` always receives the full tagged object, with nested values
|
|
95
|
+
already restored, whichever form `serialize` returned. Read `$value` from it.
|
|
96
|
+
|
|
97
|
+
**Opting out of the built-ins.** Pass `builtins: false` (to `JSONSerializer`
|
|
98
|
+
or `createSerializer`) to use only your registry. A value that no transformer
|
|
99
|
+
handles and that JSON would write as `{}` (a `Map`, `Set`, `WeakMap`, `Error`,
|
|
100
|
+
`RegExp`, `ArrayBuffer`, ...) then throws `SerializeError` under
|
|
101
|
+
`preserveTypes` instead of being silently emptied. To compose a registry by
|
|
102
|
+
hand, start from `createBuiltinTransformers()`.
|
|
103
|
+
|
|
80
104
|
## Untrusted input
|
|
81
105
|
|
|
82
106
|
`deserialize` is the boundary that matters — the string arrives from a queue, an
|
package/dist/index.d.ts
CHANGED
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
export type { SerializationFormat, SerializedValue, SerializeOptions, DeserializeOptions, SerializationMetadata, SerializedEnvelope, Serializer, TypeTransformer, } from "./serializerTypes/index.js";
|
|
28
28
|
export { JSONSerializer, ESCAPED_OBJECT_TAG } from "./serializerJson/index.js";
|
|
29
|
+
export type { JSONSerializerOptions } from "./serializerJson/index.js";
|
|
29
30
|
export { TransformerRegistry } from "./serializerTransforms/index.js";
|
|
30
31
|
export { DateTransformer } from "./serializerTransforms/index.js";
|
|
31
32
|
export { BigIntTransformer } from "./serializerTransforms/index.js";
|
|
32
33
|
export { MapTransformer } from "./serializerTransforms/index.js";
|
|
33
34
|
export { SetTransformer } from "./serializerTransforms/index.js";
|
|
35
|
+
export { createBuiltinTransformers } from "./serializerTransformPolicy/index.js";
|
|
34
36
|
export { BufferTransformer } from "./serializerTransformsExt/index.js";
|
|
35
37
|
export { ErrorTransformer } from "./serializerTransformsExt/index.js";
|
|
36
38
|
export { toBase64, fromBase64, encodeUtf8, decodeUtf8, } from "./serializerTransformsExt/index.js";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { DateTransformer } from "./serializerTransforms/index.js";
|
|
|
32
32
|
export { BigIntTransformer } from "./serializerTransforms/index.js";
|
|
33
33
|
export { MapTransformer } from "./serializerTransforms/index.js";
|
|
34
34
|
export { SetTransformer } from "./serializerTransforms/index.js";
|
|
35
|
+
export { createBuiltinTransformers } from "./serializerTransformPolicy/index.js";
|
|
35
36
|
// ─── Extended Transformers ────────────────────────────────────
|
|
36
37
|
export { BufferTransformer } from "./serializerTransformsExt/index.js";
|
|
37
38
|
export { ErrorTransformer } from "./serializerTransformsExt/index.js";
|
|
@@ -30,7 +30,10 @@ export declare function contentTypeForFormat(format: string): string;
|
|
|
30
30
|
* Validates that a value received from the wire is a well-formed envelope.
|
|
31
31
|
*
|
|
32
32
|
* @param envelope - The candidate envelope.
|
|
33
|
-
* @throws {
|
|
33
|
+
* @throws {InvalidSerializedDataError} when the shape or schema version is
|
|
34
|
+
* unusable. Envelope payloads arrive from a queue or an RPC peer, so a
|
|
35
|
+
* rejection has to be distinguishable from an internal bug: a bare `Error`
|
|
36
|
+
* left callers unable to tell hostile input from a defect of their own.
|
|
34
37
|
*/
|
|
35
38
|
export declare function assertValidEnvelope(envelope: unknown): asserts envelope is SerializedEnvelope;
|
|
36
39
|
/**
|
|
@@ -39,7 +42,8 @@ export declare function assertValidEnvelope(envelope: unknown): asserts envelope
|
|
|
39
42
|
* @param envelope - The envelope to unwrap.
|
|
40
43
|
* @param expectedFormat - Optional format to validate against.
|
|
41
44
|
* @returns The raw serialized data.
|
|
42
|
-
* @throws {
|
|
45
|
+
* @throws {InvalidSerializedDataError} when the envelope is malformed or its
|
|
46
|
+
* format doesn't match expectations.
|
|
43
47
|
*/
|
|
44
48
|
export declare function unwrapEnvelope(envelope: SerializedEnvelope, expectedFormat?: string): SerializedValue;
|
|
45
49
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* messaging, queues, RPC, and cross-service communication.
|
|
7
7
|
*/
|
|
8
8
|
import { SerializationFormat, SerializationContentType, SERIALIZATION_SCHEMA_VERSION, } from "@zudojs/constants";
|
|
9
|
+
import { InvalidSerializedDataError } from "@zudojs/errors";
|
|
9
10
|
import { decodeUtf8 } from "../serializerTransformsExt/index.js";
|
|
10
11
|
/**
|
|
11
12
|
* Create a serialization envelope wrapping data with metadata.
|
|
@@ -51,32 +52,35 @@ export function contentTypeForFormat(format) {
|
|
|
51
52
|
* Validates that a value received from the wire is a well-formed envelope.
|
|
52
53
|
*
|
|
53
54
|
* @param envelope - The candidate envelope.
|
|
54
|
-
* @throws {
|
|
55
|
+
* @throws {InvalidSerializedDataError} when the shape or schema version is
|
|
56
|
+
* unusable. Envelope payloads arrive from a queue or an RPC peer, so a
|
|
57
|
+
* rejection has to be distinguishable from an internal bug: a bare `Error`
|
|
58
|
+
* left callers unable to tell hostile input from a defect of their own.
|
|
55
59
|
*/
|
|
56
60
|
export function assertValidEnvelope(envelope) {
|
|
57
61
|
if (typeof envelope !== "object" || envelope === null) {
|
|
58
|
-
throw new
|
|
62
|
+
throw new InvalidSerializedDataError(`Malformed envelope: expected an object, got ${envelope === null ? "null" : typeof envelope}`, { format: "envelope" });
|
|
59
63
|
}
|
|
60
64
|
const candidate = envelope;
|
|
61
65
|
if (typeof candidate.metadata !== "object" || candidate.metadata === null) {
|
|
62
|
-
throw new
|
|
66
|
+
throw new InvalidSerializedDataError("Malformed envelope: missing metadata", { format: "envelope" });
|
|
63
67
|
}
|
|
64
68
|
const metadata = candidate.metadata;
|
|
65
69
|
if (typeof metadata.format !== "string" || metadata.format.length === 0) {
|
|
66
|
-
throw new
|
|
70
|
+
throw new InvalidSerializedDataError("Malformed envelope: metadata.format is missing", { format: "envelope" });
|
|
67
71
|
}
|
|
68
72
|
if (typeof candidate.data !== "string" &&
|
|
69
73
|
!(candidate.data instanceof Uint8Array)) {
|
|
70
|
-
throw new
|
|
74
|
+
throw new InvalidSerializedDataError("Malformed envelope: data must be a string or Uint8Array", { format: "envelope" });
|
|
71
75
|
}
|
|
72
76
|
// The version exists so a future producer can be detected rather than
|
|
73
77
|
// silently misread. Older versions stay readable; newer ones do not.
|
|
74
78
|
if (metadata.version !== undefined) {
|
|
75
79
|
if (!Number.isInteger(metadata.version)) {
|
|
76
|
-
throw new
|
|
80
|
+
throw new InvalidSerializedDataError(`Malformed envelope: metadata.version must be an integer, got ${String(metadata.version)}`, { format: "envelope" });
|
|
77
81
|
}
|
|
78
82
|
if (metadata.version > SERIALIZATION_SCHEMA_VERSION) {
|
|
79
|
-
throw new
|
|
83
|
+
throw new InvalidSerializedDataError(`Unsupported envelope schema version ${metadata.version}: this build understands up to ${SERIALIZATION_SCHEMA_VERSION}`, { format: "envelope" });
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
}
|
|
@@ -86,12 +90,13 @@ export function assertValidEnvelope(envelope) {
|
|
|
86
90
|
* @param envelope - The envelope to unwrap.
|
|
87
91
|
* @param expectedFormat - Optional format to validate against.
|
|
88
92
|
* @returns The raw serialized data.
|
|
89
|
-
* @throws {
|
|
93
|
+
* @throws {InvalidSerializedDataError} when the envelope is malformed or its
|
|
94
|
+
* format doesn't match expectations.
|
|
90
95
|
*/
|
|
91
96
|
export function unwrapEnvelope(envelope, expectedFormat) {
|
|
92
97
|
assertValidEnvelope(envelope);
|
|
93
98
|
if (expectedFormat && envelope.metadata.format !== expectedFormat) {
|
|
94
|
-
throw new
|
|
99
|
+
throw new InvalidSerializedDataError(`Envelope format mismatch: expected "${expectedFormat}", got "${envelope.metadata.format}"`, { format: expectedFormat });
|
|
95
100
|
}
|
|
96
101
|
return envelope.data;
|
|
97
102
|
}
|
|
@@ -132,7 +137,7 @@ export function deserializeFromEnvelope(envelope, deserializer, expectedFormat,
|
|
|
132
137
|
}
|
|
133
138
|
const encoding = (envelope.metadata.encoding ?? "utf-8").toLowerCase();
|
|
134
139
|
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
135
|
-
throw new
|
|
140
|
+
throw new InvalidSerializedDataError(`Unsupported envelope encoding "${envelope.metadata.encoding}": only UTF-8 is supported`, { format: envelope.metadata.format });
|
|
136
141
|
}
|
|
137
142
|
return deserializer.deserialize(decodeUtf8(data), options);
|
|
138
143
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* JSON serializer with fast path (native JSON) and advanced path
|
|
5
5
|
* with type preservation via transformer registry.
|
|
6
6
|
*/
|
|
7
|
-
export { JSONSerializer } from "./jsonSerializer.core.js";
|
|
7
|
+
export { JSONSerializer, type JSONSerializerOptions, } from "./jsonSerializer.core.js";
|
|
8
8
|
export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* JSON serializer with fast path (native JSON) and advanced path
|
|
5
5
|
* with type preservation via transformer registry.
|
|
6
6
|
*/
|
|
7
|
-
export { JSONSerializer } from "./jsonSerializer.core.js";
|
|
7
|
+
export { JSONSerializer, } from "./jsonSerializer.core.js";
|
|
8
8
|
export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Serializer, SerializeOptions, DeserializeOptions } from "../serializerTypes/index.js";
|
|
9
9
|
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
10
|
+
/** Options accepted by the {@link JSONSerializer} constructor. */
|
|
11
|
+
export interface JSONSerializerOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Your transformers. They are consulted first; the built-in transformers
|
|
14
|
+
* (`Date`, `BigInt`, `Map`, `Set`, `Uint8Array`/`Buffer`, `Error`) still
|
|
15
|
+
* handle every type your registry does not, unless `builtins` is `false`.
|
|
16
|
+
* Transformers registered on it later are picked up.
|
|
17
|
+
*/
|
|
18
|
+
readonly transformers?: TransformerRegistry;
|
|
19
|
+
/**
|
|
20
|
+
* Include the built-in transformers behind `transformers` (default
|
|
21
|
+
* `true`). With `false` only your registry is used, and a value no
|
|
22
|
+
* transformer handles that JSON would write as `{}` (a `Map`, `Set`,
|
|
23
|
+
* `Error`, ...) throws `SerializeError` under `preserveTypes`.
|
|
24
|
+
*/
|
|
25
|
+
readonly builtins?: boolean;
|
|
26
|
+
/** Default options merged into every call. */
|
|
27
|
+
readonly defaults?: SerializeOptions & DeserializeOptions;
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
12
31
|
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
@@ -16,13 +35,10 @@ import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
|
16
35
|
export declare class JSONSerializer implements Serializer<unknown, string> {
|
|
17
36
|
readonly name = "json";
|
|
18
37
|
readonly contentType = "application/json";
|
|
38
|
+
private readonly registry;
|
|
19
39
|
private readonly transformers;
|
|
20
40
|
private readonly defaults;
|
|
21
|
-
constructor(options?:
|
|
22
|
-
readonly transformers?: TransformerRegistry;
|
|
23
|
-
/** Default options merged into every call. */
|
|
24
|
-
readonly defaults?: SerializeOptions & DeserializeOptions;
|
|
25
|
-
});
|
|
41
|
+
constructor(options?: JSONSerializerOptions);
|
|
26
42
|
/**
|
|
27
43
|
* Register a custom type transformer.
|
|
28
44
|
*
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Advanced path (preserveTypes: true): recursive traversal with
|
|
6
6
|
* transformer-based type preservation.
|
|
7
7
|
*/
|
|
8
|
-
import { TransformerRegistry
|
|
9
|
-
import {
|
|
8
|
+
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
9
|
+
import { createBuiltinTransformers, layerTransformers, } from "../serializerTransformPolicy/index.js";
|
|
10
10
|
import { SerializationLimits } from "@zudojs/constants";
|
|
11
11
|
import { InvalidSerializedDataError, TransformerError } from "@zudojs/errors";
|
|
12
12
|
import { assertNoCircularReference, assertDepthWithinLimit, } from "@zudojs/validation";
|
|
@@ -14,17 +14,6 @@ import { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
|
14
14
|
import { assertByteSize } from "./jsonSerializer.keys.js";
|
|
15
15
|
import { transformValue } from "./jsonSerializer.transform.js";
|
|
16
16
|
import { restoreValue } from "./jsonSerializer.restore.js";
|
|
17
|
-
/** Default transformer registry with all built-in transformers. */
|
|
18
|
-
function createDefaultTransformers() {
|
|
19
|
-
const registry = new TransformerRegistry();
|
|
20
|
-
registry.register(DateTransformer);
|
|
21
|
-
registry.register(BigIntTransformer);
|
|
22
|
-
registry.register(MapTransformer);
|
|
23
|
-
registry.register(SetTransformer);
|
|
24
|
-
registry.register(BufferTransformer);
|
|
25
|
-
registry.register(ErrorTransformer);
|
|
26
|
-
return registry;
|
|
27
|
-
}
|
|
28
17
|
/**
|
|
29
18
|
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
30
19
|
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
@@ -34,10 +23,18 @@ function createDefaultTransformers() {
|
|
|
34
23
|
export class JSONSerializer {
|
|
35
24
|
name = "json";
|
|
36
25
|
contentType = "application/json";
|
|
26
|
+
registry;
|
|
37
27
|
transformers;
|
|
38
28
|
defaults;
|
|
39
29
|
constructor(options) {
|
|
40
|
-
|
|
30
|
+
const builtins = options?.builtins !== false;
|
|
31
|
+
const custom = options?.transformers;
|
|
32
|
+
this.registry =
|
|
33
|
+
custom ?? (builtins ? createBuiltinTransformers() : new TransformerRegistry());
|
|
34
|
+
this.transformers =
|
|
35
|
+
custom && builtins
|
|
36
|
+
? layerTransformers(custom, createBuiltinTransformers())
|
|
37
|
+
: this.registry;
|
|
41
38
|
this.defaults = options?.defaults ?? {};
|
|
42
39
|
}
|
|
43
40
|
/**
|
|
@@ -49,14 +46,18 @@ export class JSONSerializer {
|
|
|
49
46
|
if (transformer.type === ESCAPED_OBJECT_TAG) {
|
|
50
47
|
throw new TransformerError(transformer.type, `"${ESCAPED_OBJECT_TAG}" is reserved for escaped plain objects.`);
|
|
51
48
|
}
|
|
52
|
-
this.
|
|
49
|
+
this.registry.register(transformer);
|
|
53
50
|
}
|
|
54
51
|
serialize(value, options) {
|
|
55
52
|
const opts = { ...this.defaults, ...options };
|
|
56
53
|
const maxDepth = opts.maxDepth ?? SerializationLimits.MAX_DEPTH;
|
|
57
54
|
const maxSize = opts.maxSize ?? SerializationLimits.MAX_SIZE;
|
|
58
55
|
if (opts.preserveTypes === true) {
|
|
59
|
-
|
|
56
|
+
// The cycle check runs first and must therefore honour the caller's
|
|
57
|
+
// depth limit: with its own 512-level ceiling it halted on a deep but
|
|
58
|
+
// perfectly acyclic payload and reported a cycle that did not exist,
|
|
59
|
+
// disagreeing with the fast path below for the same input.
|
|
60
|
+
assertNoCircularReference(value, "root", maxDepth);
|
|
60
61
|
assertDepthWithinLimit(value, maxDepth);
|
|
61
62
|
const transformed = transformValue({ transformers: this.transformers, maxDepth, options: opts }, value, 0);
|
|
62
63
|
const json = opts.pretty
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* plain objects (see `jsonSerializer.escape.ts`).
|
|
6
6
|
*/
|
|
7
7
|
import type { DeserializeOptions } from "../serializerTypes/index.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type { TransformerLookup } from "../serializerTransformPolicy/index.js";
|
|
9
9
|
/** What the walker needs besides the value. */
|
|
10
10
|
export interface RestoreWalk {
|
|
11
|
-
readonly transformers:
|
|
11
|
+
readonly transformers: TransformerLookup;
|
|
12
12
|
readonly maxDepth: number;
|
|
13
13
|
readonly options: DeserializeOptions;
|
|
14
14
|
}
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
* plain objects (see `jsonSerializer.escape.ts`).
|
|
6
6
|
*/
|
|
7
7
|
import { InvalidSerializedDataError, SerializationDepthError, TransformerError, isSerializationError, } from "@zudojs/errors";
|
|
8
|
-
import { SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { SerializationLimits, SerializationTags } from "@zudojs/constants";
|
|
9
9
|
import { isPlainObject } from "@zudojs/types";
|
|
10
10
|
import { escapedBody } from "./jsonSerializer.escape.js";
|
|
11
11
|
import { defineKey } from "./jsonSerializer.keys.js";
|
|
12
|
+
/** Longest prefix of a rejected tag quoted back in an error message. */
|
|
13
|
+
const TAG_EXCERPT_LENGTH = 64;
|
|
14
|
+
/** Clips a wire-supplied tag so it cannot flood an error message or a log line. */
|
|
15
|
+
function describeTag(tag) {
|
|
16
|
+
return tag.length <= TAG_EXCERPT_LENGTH
|
|
17
|
+
? tag
|
|
18
|
+
: `${tag.slice(0, TAG_EXCERPT_LENGTH)}…`;
|
|
19
|
+
}
|
|
12
20
|
/** Rebuilds runtime values from their tagged JSON representation. */
|
|
13
21
|
export function restoreValue(walk, value, depth) {
|
|
14
22
|
if (value === null || typeof value !== "object")
|
|
@@ -29,11 +37,19 @@ export function restoreValue(walk, value, depth) {
|
|
|
29
37
|
}
|
|
30
38
|
const tag = obj[SerializationTags.TYPE];
|
|
31
39
|
if (body === undefined && typeof tag === "string") {
|
|
40
|
+
// The tag arrives from the wire, so bound it before it is looked up or
|
|
41
|
+
// quoted into a message: `SerializationLimits.MAX_TYPE_TAG_LENGTH` was
|
|
42
|
+
// exported and tested but never enforced, which let a 100 000-character
|
|
43
|
+
// tag be interpolated verbatim into an error and from there into a log.
|
|
44
|
+
if (tag.length > SerializationLimits.MAX_TYPE_TAG_LENGTH) {
|
|
45
|
+
throw new InvalidSerializedDataError(`Serialization type tag exceeds ${SerializationLimits.MAX_TYPE_TAG_LENGTH} characters ` +
|
|
46
|
+
`(got ${tag.length}): "${describeTag(tag)}".`, { format: "json" });
|
|
47
|
+
}
|
|
32
48
|
if (walk.transformers.has(tag))
|
|
33
49
|
return revive(walk, tag, obj, depth);
|
|
34
50
|
// An unknown tag is ordinary data unless the caller asked for strictness.
|
|
35
51
|
if (strict) {
|
|
36
|
-
throw new InvalidSerializedDataError(`Unknown serialization type tag: "${tag}". ` +
|
|
52
|
+
throw new InvalidSerializedDataError(`Unknown serialization type tag: "${describeTag(tag)}". ` +
|
|
37
53
|
"Register a transformer for it, or deserialize without strict mode.", { format: "json" });
|
|
38
54
|
}
|
|
39
55
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* and escapes plain objects that would otherwise read back as a tag.
|
|
6
6
|
*/
|
|
7
7
|
import type { SerializeOptions } from "../serializerTypes/index.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type TransformerLookup } from "../serializerTransformPolicy/index.js";
|
|
9
9
|
/** What the walker needs besides the value. */
|
|
10
10
|
export interface TransformWalk {
|
|
11
|
-
readonly transformers:
|
|
11
|
+
readonly transformers: TransformerLookup;
|
|
12
12
|
readonly maxDepth: number;
|
|
13
13
|
readonly options: SerializeOptions;
|
|
14
14
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationDepthError } from "@zudojs/errors";
|
|
8
8
|
import { isPlainObject } from "@zudojs/types";
|
|
9
|
+
import { assertNotLossy, toTaggedOutput, } from "../serializerTransformPolicy/index.js";
|
|
9
10
|
import { escapeObject, needsEscape } from "./jsonSerializer.escape.js";
|
|
10
11
|
import { defineKey } from "./jsonSerializer.keys.js";
|
|
11
12
|
/** Converts a value into its JSON-safe, tagged representation. */
|
|
@@ -15,7 +16,7 @@ export function transformValue(walk, value, depth) {
|
|
|
15
16
|
if (typeof value === "bigint") {
|
|
16
17
|
const transformer = walk.transformers.findForValue(value);
|
|
17
18
|
return transformer
|
|
18
|
-
? transformer.serialize(value, walk.options)
|
|
19
|
+
? transformEntries(walk, toTaggedOutput(transformer, transformer.serialize(value, walk.options)), depth + 1)
|
|
19
20
|
: value.toString();
|
|
20
21
|
}
|
|
21
22
|
if (typeof value !== "object")
|
|
@@ -35,17 +36,17 @@ export function transformValue(walk, value, depth) {
|
|
|
35
36
|
}
|
|
36
37
|
const transformer = walk.transformers.findForValue(value);
|
|
37
38
|
if (transformer) {
|
|
38
|
-
const raw = transformer.serialize(value, walk.options);
|
|
39
39
|
// The transformer's own tag object is not user data: its children are
|
|
40
|
-
// walked, but it is never escaped.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// walked, but it is never escaped. A bare return value is wrapped in
|
|
41
|
+
// `{ $type, $value }` for the transformer author.
|
|
42
|
+
const raw = transformer.serialize(value, walk.options);
|
|
43
|
+
return transformEntries(walk, toTaggedOutput(transformer, raw), depth + 1);
|
|
44
44
|
}
|
|
45
45
|
if (isPlainObject(value)) {
|
|
46
46
|
const body = transformEntries(walk, value, depth);
|
|
47
47
|
return needsEscape(value) ? escapeObject(body) : body;
|
|
48
48
|
}
|
|
49
|
+
assertNotLossy(value);
|
|
49
50
|
return value;
|
|
50
51
|
}
|
|
51
52
|
/** Transforms each own enumerable key of a plain object. */
|
|
@@ -17,8 +17,13 @@ import { SerializerRegistry } from "./serializerRegistry.core.js";
|
|
|
17
17
|
* default that each call can still override.
|
|
18
18
|
*/
|
|
19
19
|
export interface CreateSerializerOptions extends SerializeOptions, DeserializeOptions {
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Your transformers, consulted before the built-in ones (which still
|
|
22
|
+
* handle every other type unless `builtins` is `false`).
|
|
23
|
+
*/
|
|
21
24
|
readonly transformers?: TransformerRegistry;
|
|
25
|
+
/** Include the built-in transformers (default `true`). */
|
|
26
|
+
readonly builtins?: boolean;
|
|
22
27
|
/** Pretty-print by default. Overridable per call. */
|
|
23
28
|
readonly pretty?: boolean;
|
|
24
29
|
/** Preserve special JS types by default. Overridable per call. */
|
|
@@ -15,9 +15,13 @@ export function createSerializer(format, options) {
|
|
|
15
15
|
case Format.JSON: {
|
|
16
16
|
// Only `pretty` and `preserveTypes` used to be forwarded, so instance
|
|
17
17
|
// limits such as `maxDepth`/`maxSize`/`strict` were silently dropped.
|
|
18
|
-
const { transformers, ...rest } = options ?? {};
|
|
18
|
+
const { transformers, builtins, ...rest } = options ?? {};
|
|
19
19
|
const defaults = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
20
|
-
return new JSONSerializer({
|
|
20
|
+
return new JSONSerializer({
|
|
21
|
+
...(transformers !== undefined ? { transformers } : {}),
|
|
22
|
+
...(builtins !== undefined ? { builtins } : {}),
|
|
23
|
+
defaults,
|
|
24
|
+
});
|
|
21
25
|
}
|
|
22
26
|
default:
|
|
23
27
|
throw new UnsupportedSerializationFormatError(format);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer policy.
|
|
3
|
+
*
|
|
4
|
+
* How the serializer resolves transformers (a caller registry layered over
|
|
5
|
+
* the built-ins), normalises what a transformer's `serialize` returns, and
|
|
6
|
+
* refuses values it would otherwise silently write as `{}`.
|
|
7
|
+
*/
|
|
8
|
+
export { createBuiltinTransformers, layerTransformers, type TransformerLookup, } from "./transformerLookup.core.js";
|
|
9
|
+
export { assertNotLossy, toTaggedOutput } from "./transformerOutput.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer policy.
|
|
3
|
+
*
|
|
4
|
+
* How the serializer resolves transformers (a caller registry layered over
|
|
5
|
+
* the built-ins), normalises what a transformer's `serialize` returns, and
|
|
6
|
+
* refuses values it would otherwise silently write as `{}`.
|
|
7
|
+
*/
|
|
8
|
+
export { createBuiltinTransformers, layerTransformers, } from "./transformerLookup.core.js";
|
|
9
|
+
export { assertNotLossy, toTaggedOutput } from "./transformerOutput.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer lookup.
|
|
3
|
+
*
|
|
4
|
+
* The serializer resolves transformers through a lookup rather than a single
|
|
5
|
+
* registry, so a caller-supplied registry can sit in front of the built-ins
|
|
6
|
+
* instead of replacing them.
|
|
7
|
+
*/
|
|
8
|
+
import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
9
|
+
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
10
|
+
/** The read side of a transformer registry, as the walkers use it. */
|
|
11
|
+
export interface TransformerLookup {
|
|
12
|
+
/** Find a transformer that can serialize the given value. */
|
|
13
|
+
findForValue(value: unknown): TypeTransformer | undefined;
|
|
14
|
+
/** Returns true when a transformer is registered for the type tag. */
|
|
15
|
+
has(type: string): boolean;
|
|
16
|
+
/** Retrieve a transformer by type tag (throws when absent). */
|
|
17
|
+
get(type: string): TypeTransformer;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a registry holding every built-in transformer: `Date`, `BigInt`,
|
|
21
|
+
* `Map`, `Set`, `Uint8Array`/`Buffer` and `Error`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createBuiltinTransformers(): TransformerRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Layers `primary` over `fallback`: every lookup consults `primary` first,
|
|
26
|
+
* so a caller's transformer for a tag or a value wins over the built-in
|
|
27
|
+
* one. `primary` is read live, so transformers registered on it later are
|
|
28
|
+
* seen too.
|
|
29
|
+
*/
|
|
30
|
+
export declare function layerTransformers(primary: TransformerLookup, fallback: TransformerLookup): TransformerLookup;
|
|
31
|
+
//# sourceMappingURL=transformerLookup.core.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer lookup.
|
|
3
|
+
*
|
|
4
|
+
* The serializer resolves transformers through a lookup rather than a single
|
|
5
|
+
* registry, so a caller-supplied registry can sit in front of the built-ins
|
|
6
|
+
* instead of replacing them.
|
|
7
|
+
*/
|
|
8
|
+
import { BigIntTransformer, DateTransformer, MapTransformer, SetTransformer, TransformerRegistry, } from "../serializerTransforms/index.js";
|
|
9
|
+
import { BufferTransformer, ErrorTransformer, } from "../serializerTransformsExt/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Creates a registry holding every built-in transformer: `Date`, `BigInt`,
|
|
12
|
+
* `Map`, `Set`, `Uint8Array`/`Buffer` and `Error`.
|
|
13
|
+
*/
|
|
14
|
+
export function createBuiltinTransformers() {
|
|
15
|
+
const registry = new TransformerRegistry();
|
|
16
|
+
registry.register(DateTransformer);
|
|
17
|
+
registry.register(BigIntTransformer);
|
|
18
|
+
registry.register(MapTransformer);
|
|
19
|
+
registry.register(SetTransformer);
|
|
20
|
+
registry.register(BufferTransformer);
|
|
21
|
+
registry.register(ErrorTransformer);
|
|
22
|
+
return registry;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Layers `primary` over `fallback`: every lookup consults `primary` first,
|
|
26
|
+
* so a caller's transformer for a tag or a value wins over the built-in
|
|
27
|
+
* one. `primary` is read live, so transformers registered on it later are
|
|
28
|
+
* seen too.
|
|
29
|
+
*/
|
|
30
|
+
export function layerTransformers(primary, fallback) {
|
|
31
|
+
return {
|
|
32
|
+
findForValue: (value) => primary.findForValue(value) ?? fallback.findForValue(value),
|
|
33
|
+
has: (type) => primary.has(type) || fallback.has(type),
|
|
34
|
+
get: (type) => (primary.has(type) ? primary.get(type) : fallback.get(type)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=transformerLookup.core.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer output and lossy-value checks.
|
|
3
|
+
*/
|
|
4
|
+
import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Normalises what a transformer's `serialize` returned into the tagged
|
|
7
|
+
* `{ $type, $value }` form that is written to the wire.
|
|
8
|
+
*
|
|
9
|
+
* A transformer may return the full tagged object (the built-ins do; any
|
|
10
|
+
* plain object with its own string `$type` is taken as-is), or just the
|
|
11
|
+
* value to store, which is wrapped as `{ $type: transformer.type, $value }`.
|
|
12
|
+
* Before, a bare return was written untagged and could never be revived.
|
|
13
|
+
*/
|
|
14
|
+
export declare function toTaggedOutput(transformer: TypeTransformer, raw: unknown): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Throws when a value that no transformer handles would be written as `{}`
|
|
17
|
+
* (a `Map`, `Set`, `Error`, `RegExp`, ...), instead of silently losing its
|
|
18
|
+
* contents. Only reachable when the built-in transformers are disabled or
|
|
19
|
+
* a type has no transformer.
|
|
20
|
+
*
|
|
21
|
+
* @throws {SerializeError} naming the type and how to fix it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertNotLossy(value: object): void;
|
|
24
|
+
//# sourceMappingURL=transformerOutput.helper.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer output and lossy-value checks.
|
|
3
|
+
*/
|
|
4
|
+
import { SerializationTags } from "@zudojs/constants";
|
|
5
|
+
import { SerializeError } from "@zudojs/errors";
|
|
6
|
+
import { isPlainObject } from "@zudojs/types";
|
|
7
|
+
/**
|
|
8
|
+
* Normalises what a transformer's `serialize` returned into the tagged
|
|
9
|
+
* `{ $type, $value }` form that is written to the wire.
|
|
10
|
+
*
|
|
11
|
+
* A transformer may return the full tagged object (the built-ins do; any
|
|
12
|
+
* plain object with its own string `$type` is taken as-is), or just the
|
|
13
|
+
* value to store, which is wrapped as `{ $type: transformer.type, $value }`.
|
|
14
|
+
* Before, a bare return was written untagged and could never be revived.
|
|
15
|
+
*/
|
|
16
|
+
export function toTaggedOutput(transformer, raw) {
|
|
17
|
+
if (isPlainObject(raw) &&
|
|
18
|
+
Object.hasOwn(raw, SerializationTags.TYPE) &&
|
|
19
|
+
typeof raw[SerializationTags.TYPE] === "string") {
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
[SerializationTags.TYPE]: transformer.type,
|
|
24
|
+
[SerializationTags.VALUE]: raw,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Built-in objects whose JSON form silently drops their contents. */
|
|
28
|
+
const LOSSY_TYPES = [
|
|
29
|
+
["Map", (value) => value instanceof Map],
|
|
30
|
+
["Set", (value) => value instanceof Set],
|
|
31
|
+
["WeakMap", (value) => value instanceof WeakMap],
|
|
32
|
+
["WeakSet", (value) => value instanceof WeakSet],
|
|
33
|
+
["WeakRef", (value) => value instanceof WeakRef],
|
|
34
|
+
["Promise", (value) => value instanceof Promise],
|
|
35
|
+
["RegExp", (value) => value instanceof RegExp],
|
|
36
|
+
["Error", (value) => value instanceof Error],
|
|
37
|
+
["ArrayBuffer", (value) => value instanceof ArrayBuffer],
|
|
38
|
+
["DataView", (value) => value instanceof DataView],
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Throws when a value that no transformer handles would be written as `{}`
|
|
42
|
+
* (a `Map`, `Set`, `Error`, `RegExp`, ...), instead of silently losing its
|
|
43
|
+
* contents. Only reachable when the built-in transformers are disabled or
|
|
44
|
+
* a type has no transformer.
|
|
45
|
+
*
|
|
46
|
+
* @throws {SerializeError} naming the type and how to fix it.
|
|
47
|
+
*/
|
|
48
|
+
export function assertNotLossy(value) {
|
|
49
|
+
for (const [name, matches] of LOSSY_TYPES) {
|
|
50
|
+
if (matches(value)) {
|
|
51
|
+
throw new SerializeError(`Cannot serialize a ${name} with preserveTypes: no transformer is ` +
|
|
52
|
+
`registered for it, and JSON would write it as {} and lose its ` +
|
|
53
|
+
`contents. Register a transformer for ${name}, or keep the ` +
|
|
54
|
+
`built-in transformers enabled (do not pass builtins: false).`, { format: "json" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=transformerOutput.helper.js.map
|
|
@@ -13,7 +13,11 @@ import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export declare class TransformerRegistry {
|
|
15
15
|
private readonly transformers;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Register a transformer.
|
|
18
|
+
*
|
|
19
|
+
* @throws {TransformerError} when the registry is already full.
|
|
20
|
+
*/
|
|
17
21
|
register(transformer: TypeTransformer): void;
|
|
18
22
|
/** Unregister a transformer by type tag. */
|
|
19
23
|
unregister(type: string): boolean;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages type transformers that handle custom JS types during
|
|
5
5
|
* serialization and deserialization. Keyed by type tag string.
|
|
6
6
|
*/
|
|
7
|
-
import { TransformerNotFoundError } from "@zudojs/errors";
|
|
7
|
+
import { TransformerError, TransformerNotFoundError } from "@zudojs/errors";
|
|
8
8
|
import { SerializationLimits } from "@zudojs/constants";
|
|
9
9
|
/**
|
|
10
10
|
* Registry of type transformers keyed by their type tag.
|
|
@@ -14,10 +14,14 @@ import { SerializationLimits } from "@zudojs/constants";
|
|
|
14
14
|
*/
|
|
15
15
|
export class TransformerRegistry {
|
|
16
16
|
transformers = new Map();
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Register a transformer.
|
|
19
|
+
*
|
|
20
|
+
* @throws {TransformerError} when the registry is already full.
|
|
21
|
+
*/
|
|
18
22
|
register(transformer) {
|
|
19
23
|
if (this.transformers.size >= SerializationLimits.MAX_TRANSFORMERS) {
|
|
20
|
-
throw new
|
|
24
|
+
throw new TransformerError(transformer.type, `Maximum transformer limit (${SerializationLimits.MAX_TRANSFORMERS}) reached.`);
|
|
21
25
|
}
|
|
22
26
|
this.transformers.set(transformer.type, transformer);
|
|
23
27
|
}
|
|
@@ -86,15 +86,34 @@ export interface Serializer<TValue = unknown, TSerialized = SerializedValue> {
|
|
|
86
86
|
/** Deserialize a value from the target format. */
|
|
87
87
|
deserialize<T = TValue>(value: TSerialized, options?: DeserializeOptions): T;
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Transforms a specific JS type during serialization.
|
|
91
|
+
*
|
|
92
|
+
* On the wire a transformed value is always the tagged object
|
|
93
|
+
* `{ "$type": type, "$value": ... }`.
|
|
94
|
+
*/
|
|
90
95
|
export interface TypeTransformer<TValue = unknown> {
|
|
91
96
|
/** Tag name used in tagged representations (e.g., "Date"). */
|
|
92
97
|
readonly type: string;
|
|
93
98
|
/** Returns true when this transformer handles the given value. */
|
|
94
99
|
canSerialize(value: unknown): value is TValue;
|
|
95
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Convert the value into a JSON-safe representation.
|
|
102
|
+
*
|
|
103
|
+
* Return either just the value to store (for example
|
|
104
|
+
* `v.toString()`), which the serializer wraps as
|
|
105
|
+
* `{ $type: type, $value: <returned> }`, or the full tagged object
|
|
106
|
+
* `{ $type: type, $value: ... }` yourself (as the built-ins do). A plain
|
|
107
|
+
* object with its own string `$type` is taken to be the full tagged
|
|
108
|
+
* object; anything else is wrapped. Nested special values inside the
|
|
109
|
+
* returned value are transformed too.
|
|
110
|
+
*/
|
|
96
111
|
serialize(value: TValue, options?: SerializeOptions): unknown;
|
|
97
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Reconstruct the original value. Always receives the full tagged object
|
|
114
|
+
* `{ $type, $value }` (with nested values already restored), whichever
|
|
115
|
+
* form `serialize` returned, so read `$value` from it.
|
|
116
|
+
*/
|
|
98
117
|
deserialize(value: unknown, options?: DeserializeOptions): TValue;
|
|
99
118
|
}
|
|
100
119
|
//# sourceMappingURL=serializer.type.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/serialization",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Data translation layer with JSON serializer, type transformers, envelopes, and registry for Zudojs applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zudojs/constants": "1.1.
|
|
23
|
-
"@zudojs/errors": "1.
|
|
24
|
-
"@zudojs/types": "1.
|
|
25
|
-
"@zudojs/validation": "1.0
|
|
22
|
+
"@zudojs/constants": "1.1.2",
|
|
23
|
+
"@zudojs/errors": "1.3.0",
|
|
24
|
+
"@zudojs/types": "1.2.0",
|
|
25
|
+
"@zudojs/validation": "1.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "7.0.2",
|
|
29
|
-
"vitest": "^
|
|
29
|
+
"vitest": "^5.0.1"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"author": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=24.0.0"
|
|
47
47
|
},
|
|
48
48
|
"module": "./dist/index.js",
|
|
49
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-serialization",
|
|
50
50
|
"bugs": {
|
|
51
51
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
52
52
|
},
|