@tsed/json-mapper 8.44.2 → 8.44.4

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.
@@ -1,12 +1,35 @@
1
1
  import { JsonMapperCompiler } from "./JsonMapperCompiler.js";
2
2
  import { JsonParameterStore, JsonSchema, VendorKeys, getPropertiesStores, s } from "@tsed/schema";
3
- import { classOf, isArray, isBoolean, isEmpty, isNil, nameOf, objectKeys } from "@tsed/core";
3
+ import { classOf, isArray, isBoolean, isEmpty, isFunction, isNil, nameOf, objectKeys } from "@tsed/core";
4
4
  import { JsonMapperSettings } from "./JsonMapperSettings.js";
5
5
  import { Writer } from "./Writer.js";
6
6
  import { alterAfterDeserialize } from "../hooks/alterAfterDeserialize.js";
7
7
  import { alterBeforeDeserialize } from "../hooks/alterBeforeDeserialize.js";
8
8
  import { alterOnDeserialize } from "../hooks/alterOnDeserialize.js";
9
9
  import { getJsonMapperTypes } from "./JsonMapperTypesContainer.js";
10
+ /**
11
+ * Whether the mapper has a callable method named after the type. A property set to another value (e.g. `undefined`
12
+ * to disable the conversion) falls back to `deserialize()` instead of throwing.
13
+ */
14
+ function hasConversionMethod(mapper, name) {
15
+ return isFunction(mapper[name]);
16
+ }
17
+ /**
18
+ * The registered function reads the mapper methods at call time, so a replaced method needs no registration.
19
+ */
20
+ function createTypeMapperEntry(mapper, model, name) {
21
+ const hasConversion = hasConversionMethod(mapper, name);
22
+ const fn = hasConversion
23
+ ? (value, options) => mapper[name](value, options)
24
+ : (value, options) => mapper.deserialize(value, { ...options, type: model });
25
+ return { mapper, name, hasConversion, fn };
26
+ }
27
+ /**
28
+ * The function must be built again when the mapper gained or lost the method named after the type.
29
+ */
30
+ function hasTypeMapperChanged(entry, mapper) {
31
+ return entry.hasConversion !== hasConversionMethod(mapper, entry.name);
32
+ }
10
33
  function isDeserializable(obj, options) {
11
34
  if ((!!options.collectionType && isNil(obj)) || obj === undefined) {
12
35
  return false;
@@ -203,21 +226,7 @@ export class JsonDeserializer extends JsonMapperCompiler {
203
226
  return this.mapOptions(mapParamStoreOptions(options.store, options));
204
227
  }
205
228
  const customMappers = {};
206
- types = types || getJsonMapperTypes();
207
- types.forEach((mapper, model) => {
208
- if (![Array, Set, Map].includes(model)) {
209
- const typeName = nameOf(model);
210
- if (nameOf(model) in mapper) {
211
- this.addTypeMapper(model, mapper[typeName].bind(mapper));
212
- }
213
- else {
214
- this.addTypeMapper(model, (value, options) => mapper.deserialize(value, {
215
- ...options,
216
- type: model
217
- }));
218
- }
219
- }
220
- });
229
+ this.syncTypeMappers(types || getJsonMapperTypes(), createTypeMapperEntry, hasTypeMapperChanged);
221
230
  const strictGroups = options.strictGroups ?? JsonMapperSettings.strictGroups;
222
231
  return {
223
232
  ...options,
@@ -254,37 +263,45 @@ export class JsonDeserializer extends JsonMapperCompiler {
254
263
  mapObject(input, options) {
255
264
  return input;
256
265
  }
257
- mapSet(input, options) {
266
+ // The collection mappers strip `id` from the options once, not once per item:
267
+ // `id` describes the items, it must not leak into the options of a nested mapper.
268
+ mapSet(input, { id, ...options }) {
258
269
  if (isNil(input)) {
259
270
  return input;
260
271
  }
261
272
  const obj = new Set();
262
- objectKeys(input).forEach((key) => {
263
- obj.add(this.mapItem(input[key], options));
264
- });
273
+ for (const key of objectKeys(input)) {
274
+ obj.add(this.execMapper(id, input[key], options));
275
+ }
265
276
  return obj;
266
277
  }
267
- mapArray(input, options) {
278
+ mapArray(input, { id, ...options }) {
268
279
  if (isNil(input)) {
269
280
  return input;
270
281
  }
271
- return [].concat(input).map((item) => {
272
- return this.mapItem(item, options);
273
- });
282
+ // a single value is mapped as a one-item array
283
+ const list = isArray(input) ? input : [input];
284
+ const out = new Array(list.length);
285
+ for (let i = 0; i < list.length; i++) {
286
+ const item = list[i];
287
+ // keep the holes of a sparse array: the item mapper is not called for them
288
+ if (item === undefined && !(i in list)) {
289
+ continue;
290
+ }
291
+ out[i] = this.execMapper(id, item, options);
292
+ }
293
+ return out;
274
294
  }
275
- mapMap(input, options) {
295
+ mapMap(input, { id, ...options }) {
276
296
  if (isNil(input)) {
277
297
  return input;
278
298
  }
279
299
  const obj = new Map();
280
- objectKeys(input).forEach((key) => {
281
- obj.set(key, this.mapItem(input[key], options));
282
- });
300
+ for (const key of objectKeys(input)) {
301
+ obj.set(key, this.execMapper(id, input[key], options));
302
+ }
283
303
  return obj;
284
304
  }
285
- mapItem(input, { id, ...options }) {
286
- return this.execMapper(id, input, options);
287
- }
288
305
  alterBeforeDeserialize(schemaId, value, options) {
289
306
  return alterBeforeDeserialize(value, this.schemes[schemaId], options);
290
307
  }
@@ -17,6 +17,11 @@ export class JsonMapperCompiler {
17
17
  * @protected
18
18
  */
19
19
  this.types = new Map();
20
+ /**
21
+ * The type mappers registered from the types container, with the function each one was registered with
22
+ * @protected
23
+ */
24
+ this.typeMappers = new Map();
20
25
  /**
21
26
  * Cached executable mappers by his id
22
27
  * @protected
@@ -124,6 +129,38 @@ export class JsonMapperCompiler {
124
129
  }
125
130
  return storeGroups.get(groupsId);
126
131
  }
132
+ /**
133
+ * Register the type mappers of the types container (Array, Set and Map excepted: the compiler maps them itself).
134
+ * A type mapper is registered again only when it is outdated, so `map()` does not rebuild them on every call.
135
+ *
136
+ * @param types The type mappers by model.
137
+ * @param create Build the entry, and the function, a type mapper is registered with.
138
+ * @param hasChanged Tell whether the mapper changed since its entry was built, beyond being replaced by another mapper.
139
+ */
140
+ syncTypeMappers(types, create, hasChanged) {
141
+ types.forEach((mapper, model) => {
142
+ if (model !== Array && model !== Set && model !== Map && this.isTypeMapperOutdated(model, mapper, hasChanged)) {
143
+ const entry = create(mapper, model, nameOf(model));
144
+ this.addTypeMapper(model, entry.fn);
145
+ this.typeMappers.set(model, entry);
146
+ }
147
+ });
148
+ }
149
+ /**
150
+ * A type mapper must be registered again when it was never registered, when the mapper was replaced or changed,
151
+ * or when its entry was replaced or removed from the cache.
152
+ */
153
+ isTypeMapperOutdated(model, mapper, hasChanged) {
154
+ const registered = this.typeMappers.get(model);
155
+ if (!registered) {
156
+ return true;
157
+ }
158
+ return (registered.mapper !== mapper ||
159
+ hasChanged(registered, mapper) ||
160
+ // two models with the same name (Date and "Date") share the mapper table entry: the last registered one wins
161
+ this.cache.get(model)?.get("typeMapper")?.fn !== registered.fn ||
162
+ this.mappers[registered.name] !== registered.fn);
163
+ }
127
164
  execMapper(id, value, options) {
128
165
  if (isObjectID(value)) {
129
166
  return value.toString();
@@ -55,6 +55,21 @@ function createTypeMapper(mapper, model, name) {
55
55
  type: model
56
56
  });
57
57
  }
58
+ function createTypeMapperEntry(mapper, model, name) {
59
+ return {
60
+ mapper,
61
+ name,
62
+ serialize: mapper.serialize,
63
+ conversion: mapper[name],
64
+ fn: createTypeMapper(mapper, model, name)
65
+ };
66
+ }
67
+ /**
68
+ * The function of a type mapper is built from its `serialize` and conversion methods: it must be built again when one changed.
69
+ */
70
+ function hasTypeMapperChanged(entry, mapper) {
71
+ return entry.serialize !== mapper.serialize || entry.conversion !== mapper[entry.name];
72
+ }
58
73
  /**
59
74
  * Serializes data structures according to the metadata captured by `@tsed/schema`.
60
75
  *
@@ -64,11 +79,6 @@ function createTypeMapper(mapper, model, name) {
64
79
  export class JsonSerializer extends JsonMapperCompiler {
65
80
  constructor() {
66
81
  super();
67
- /**
68
- * The type mappers registered from the types container, with the function each one was registered with
69
- * @protected
70
- */
71
- this.typeMappers = new Map();
72
82
  this.addTypeMapper(Object, this.mapObject.bind(this));
73
83
  this.addTypeMapper(Array, this.mapArray.bind(this));
74
84
  this.addTypeMapper(Map, this.mapMap.bind(this));
@@ -117,31 +127,9 @@ export class JsonSerializer extends JsonMapperCompiler {
117
127
  writer.add(this.mapAdditionalProperties(entity, properties));
118
128
  return writer.return("obj").root().toString();
119
129
  }
120
- /**
121
- * A type mapper must be registered again when it was never registered, when the mapper or one of the
122
- * methods its function was built from changed, or when its entry was replaced or removed from the cache.
123
- */
124
- isTypeMapperOutdated(model, mapper) {
125
- const registered = this.typeMappers.get(model);
126
- if (!registered) {
127
- return true;
128
- }
129
- const mapperChanged = registered.mapper !== mapper;
130
- const methodsChanged = registered.serialize !== mapper.serialize || registered.conversion !== mapper[registered.name];
131
- const entryReplaced = this.cache.get(model)?.get("typeMapper")?.fn !== registered.fn;
132
- return mapperChanged || methodsChanged || entryReplaced;
133
- }
134
130
  mapOptions({ groups, useAlias = true, types, ...options }) {
135
131
  const customMappers = {};
136
- types = types || getJsonMapperTypes();
137
- types.forEach((mapper, model) => {
138
- if (![Array, Set, Map].includes(model) && this.isTypeMapperOutdated(model, mapper)) {
139
- const name = nameOf(model);
140
- const fn = createTypeMapper(mapper, model, name);
141
- this.addTypeMapper(model, fn);
142
- this.typeMappers.set(model, { mapper, name, serialize: mapper.serialize, conversion: mapper[name], fn });
143
- }
144
- });
132
+ this.syncTypeMappers(types || getJsonMapperTypes(), createTypeMapperEntry, hasTypeMapperChanged);
145
133
  const strictGroups = options.strictGroups ?? JsonMapperSettings.strictGroups;
146
134
  return {
147
135
  ...options,
@@ -32,7 +32,6 @@ export declare class JsonDeserializer extends JsonMapperCompiler<JsonDeserialize
32
32
  private mapSet;
33
33
  private mapArray;
34
34
  private mapMap;
35
- private mapItem;
36
35
  private alterBeforeDeserialize;
37
36
  private alterAfterDeserialize;
38
37
  private mapGenericsOptions;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@tsed/core";
2
2
  import { JsonSchema } from "@tsed/schema";
3
+ import type { JsonMapperMethods } from "../interfaces/JsonMapperMethods.js";
3
4
  /**
4
5
  * Signature exposed by compiled mappers. They accept an input value plus contextual options
5
6
  * and return the serialized/deserialized result.
@@ -16,6 +17,15 @@ export type CachedJsonMapper<Options> = {
16
17
  * Cached mapper registry keyed by the groups fingerprint generated for a schema.
17
18
  */
18
19
  export type CachedGroupsJsonMapper<Options> = Map<string, CachedJsonMapper<Options>>;
20
+ /**
21
+ * A type mapper registered from the types container: the mapper, the name of its model and the function it was
22
+ * registered with. Subclasses add the values their outdated check compares.
23
+ */
24
+ export type TypeMapperEntry = {
25
+ mapper: JsonMapperMethods;
26
+ name: string;
27
+ fn: JsonMapperCallback<any>;
28
+ };
19
29
  /**
20
30
  * Base compiler that turns schema metadata into executable (de)serialization functions.
21
31
  * Subclasses supply `map`, `alterValue`, and `createMapper` implementations to specialize
@@ -33,6 +43,11 @@ export declare abstract class JsonMapperCompiler<Options extends Record<string,
33
43
  * @protected
34
44
  */
35
45
  protected types: Map<Type<any>, Type<any>>;
46
+ /**
47
+ * The type mappers registered from the types container, with the function each one was registered with
48
+ * @protected
49
+ */
50
+ protected typeMappers: Map<any, TypeMapperEntry>;
36
51
  /**
37
52
  * Cached executable mappers by his id
38
53
  * @protected
@@ -69,6 +84,20 @@ export declare abstract class JsonMapperCompiler<Options extends Record<string,
69
84
  compile(model: Type<any> | string, groups: false | string[], opts?: {
70
85
  mapper?: any;
71
86
  }): CachedJsonMapper<Options>;
87
+ /**
88
+ * Register the type mappers of the types container (Array, Set and Map excepted: the compiler maps them itself).
89
+ * A type mapper is registered again only when it is outdated, so `map()` does not rebuild them on every call.
90
+ *
91
+ * @param types The type mappers by model.
92
+ * @param create Build the entry, and the function, a type mapper is registered with.
93
+ * @param hasChanged Tell whether the mapper changed since its entry was built, beyond being replaced by another mapper.
94
+ */
95
+ protected syncTypeMappers<Entry extends TypeMapperEntry>(types: Map<Type | Symbol | string, JsonMapperMethods>, create: (mapper: JsonMapperMethods, model: any, name: string) => Entry, hasChanged: (entry: Entry, mapper: JsonMapperMethods) => boolean): void;
96
+ /**
97
+ * A type mapper must be registered again when it was never registered, when the mapper was replaced or changed,
98
+ * or when its entry was replaced or removed from the cache.
99
+ */
100
+ private isTypeMapperOutdated;
72
101
  protected execMapper(id: string, value: any, options: Options): any;
73
102
  protected abstract map(input: any, options: Options): any;
74
103
  protected abstract alterValue(schemaId: string, value: any, options: Options): any;
@@ -1,14 +1,6 @@
1
1
  import { Type } from "@tsed/core";
2
- import { JsonMapperMethods } from "../interfaces/JsonMapperMethods.js";
3
- import { JsonMapperCompiler, JsonMapperCallback } from "./JsonMapperCompiler.js";
2
+ import { JsonMapperCompiler } from "./JsonMapperCompiler.js";
4
3
  import { JsonSerializerOptions } from "./JsonSerializerOptions.js";
5
- type TypeMapperEntry = {
6
- mapper: JsonMapperMethods;
7
- name: string;
8
- serialize: any;
9
- conversion: any;
10
- fn: JsonMapperCallback<any>;
11
- };
12
4
  /**
13
5
  * Serializes data structures according to the metadata captured by `@tsed/schema`.
14
6
  *
@@ -16,20 +8,10 @@ type TypeMapperEntry = {
16
8
  * registered type mappers (e.g., components in `src/components`).
17
9
  */
18
10
  export declare class JsonSerializer extends JsonMapperCompiler<JsonSerializerOptions> {
19
- /**
20
- * The type mappers registered from the types container, with the function each one was registered with
21
- * @protected
22
- */
23
- protected typeMappers: Map<any, TypeMapperEntry>;
24
11
  constructor();
25
12
  map(input: any, options?: JsonSerializerOptions): any;
26
13
  protected alterValue(schemaId: string, value: any, options: JsonSerializerOptions): any;
27
14
  protected createMapper(model: Type<any>, id: string, groups: false | string[]): string;
28
- /**
29
- * A type mapper must be registered again when it was never registered, when the mapper or one of the
30
- * methods its function was built from changed, or when its entry was replaced or removed from the cache.
31
- */
32
- private isTypeMapperOutdated;
33
15
  private mapOptions;
34
16
  private mapProperty;
35
17
  private getPropertyFiller;
@@ -44,4 +26,3 @@ export declare class JsonSerializer extends JsonMapperCompiler<JsonSerializerOpt
44
26
  private mapItem;
45
27
  private mapJSON;
46
28
  }
47
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/json-mapper",
3
- "version": "8.44.2",
3
+ "version": "8.44.4",
4
4
  "description": "Json mapper module for Ts.ED Framework",
5
5
  "keywords": [
6
6
  "TypeScript",
@@ -41,10 +41,10 @@
41
41
  "tslib": ">=2.7.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@tsed/barrels": "8.44.2",
45
- "@tsed/core": "8.44.2",
46
- "@tsed/schema": "8.44.2",
47
- "@tsed/typescript": "8.44.2",
44
+ "@tsed/barrels": "8.44.4",
45
+ "@tsed/core": "8.44.4",
46
+ "@tsed/schema": "8.44.4",
47
+ "@tsed/typescript": "8.44.4",
48
48
  "temporal-polyfill": "1.0.1",
49
49
  "typescript": "6.0.2",
50
50
  "vitest": "4.1.4"