@tsed/json-mapper 8.44.1 → 8.44.2

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,6 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { JsonMapper } from "../decorators/jsonMapper.js";
3
3
  import { isBoolean } from "@tsed/core";
4
+ const toISOString = Date.prototype.toISOString;
4
5
  /**
5
6
  * Mapper for `Date` type.
6
7
  * @jsonmapper
@@ -17,7 +18,7 @@ let DateMapper = class DateMapper {
17
18
  return new Date(data);
18
19
  }
19
20
  serialize(object) {
20
- return object ? new Date(object).toISOString() : object;
21
+ return object ? (object instanceof Date ? toISOString.call(object) : new Date(object).toISOString()) : object;
21
22
  }
22
23
  };
23
24
  DateMapper = __decorate([
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  import { JsonMapper } from "../decorators/jsonMapper.js";
3
3
  import { nameOf } from "@tsed/core";
4
4
  function isNullish(data) {
5
- return [null, "null"].includes(data);
5
+ return data === null || data === "null";
6
6
  }
7
7
  /**
8
8
  * Error thrown when a primitive conversion cannot be performed safely.
@@ -23,15 +23,17 @@ let PrimitiveMapper = class PrimitiveMapper {
23
23
  return this[nameOf(ctx.type)] ? this[nameOf(ctx.type)](data, ctx) : undefined;
24
24
  }
25
25
  serialize(object, ctx) {
26
- return this[nameOf(ctx?.type)] && typeof object !== "object" ? this[nameOf(ctx.type)](object, ctx) : object;
26
+ const name = nameOf(ctx?.type);
27
+ // inline typeof on purpose: hot path for every primitive value, cheaper than calling isObject()
28
+ return this[name] && typeof object !== "object" ? this[name](object, ctx) : object;
27
29
  }
28
30
  String(data) {
29
31
  return data === null ? null : "" + data;
30
32
  }
31
33
  Boolean(data) {
32
- if (["true", "1", true].includes(data))
34
+ if (data === "true" || data === "1" || data === true)
33
35
  return true;
34
- if (["false", "0", false].includes(data))
36
+ if (data === "false" || data === "0" || data === false)
35
37
  return false;
36
38
  if (isNullish(data))
37
39
  return null;
@@ -11,6 +11,12 @@ export class JsonMapperCompiler {
11
11
  * @protected
12
12
  */
13
13
  this.cache = new Map();
14
+ /**
15
+ * Cached result of `getType()` by model. `getType()` walks the class ancestors
16
+ * and runs for every value serialized without a schema, so it is resolved once per model.
17
+ * @protected
18
+ */
19
+ this.types = new Map();
14
20
  /**
15
21
  * Cached executable mappers by his id
16
22
  * @protected
@@ -91,7 +97,7 @@ export class JsonMapperCompiler {
91
97
  };
92
98
  }
93
99
  compile(model, groups, opts = {}) {
94
- const token = isString(model) ? model : this.getType(model);
100
+ const token = isString(model) ? model : this.resolveType(model);
95
101
  const groupsId = this.getGroupsId(groups);
96
102
  let storeGroups = this.cache.get(token) || this.cache.get(nameOf(token));
97
103
  if (!storeGroups) {
@@ -124,6 +130,14 @@ export class JsonMapperCompiler {
124
130
  }
125
131
  return this.mappers[id || nameOf(classOf(value))](value, options);
126
132
  }
133
+ resolveType(model) {
134
+ let type = this.types.get(model);
135
+ if (type === undefined) {
136
+ type = this.getType(model);
137
+ this.types.set(model, type);
138
+ }
139
+ return type;
140
+ }
127
141
  getType(model) {
128
142
  if (!model) {
129
143
  return Object;
@@ -1,14 +1,27 @@
1
1
  import { getPropertiesStores, s } from "@tsed/schema";
2
- import { classOf, hasJsonMethod, isArray, isBoolean, isClassObject, isCollection, isDate, isMomentObject, isMongooseObject, isNil, isObject, isPrimitive, nameOf } from "@tsed/core";
2
+ import { classOf, hasJsonMethod, isArray, isBoolean, isClassObject, isCollection, isDate, isFunction, isMomentObject, isMongooseObject, isNil, isObject, isPrimitive, nameOf } from "@tsed/core";
3
+ import { DateMapper } from "../components/DateMapper.js";
4
+ import { PrimitiveMapper } from "../components/PrimitiveMapper.js";
3
5
  import { JsonMapperCompiler } from "./JsonMapperCompiler.js";
4
6
  import { JsonMapperSettings } from "./JsonMapperSettings.js";
5
7
  import { Writer } from "./Writer.js";
6
8
  import { alterOnSerialize } from "../hooks/alterOnSerialize.js";
7
9
  import { getJsonMapperTypes } from "./JsonMapperTypesContainer.js";
8
- import { getObjectProperties } from "../utils/getObjectProperties.js";
9
10
  const getCollectionType = (input) => {
10
11
  return isArray(input) ? "Array" : input instanceof Set ? "Set" : input instanceof Map ? "Map" : undefined;
11
12
  };
13
+ /**
14
+ * Assign a mapped value on a plain output object. A `__proto__` key is defined as an own property:
15
+ * a plain assignment would call the `__proto__` setter, change the prototype and drop the key.
16
+ */
17
+ function setKey(obj, key, value) {
18
+ if (key === "__proto__") {
19
+ Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
20
+ }
21
+ else {
22
+ obj[key] = value;
23
+ }
24
+ }
12
25
  function getBestType(type, obj) {
13
26
  const dataType = classOf(obj);
14
27
  if (dataType && !isClassObject(dataType) && !isCollection(obj)) {
@@ -19,6 +32,29 @@ function getBestType(type, obj) {
19
32
  function varKey(k) {
20
33
  return `__${k}`;
21
34
  }
35
+ /**
36
+ * The function a type mapper is registered with. The built-in Date and primitive mappers do not read the
37
+ * context, so they are called without building one: a copy of the options for every Date, string and number
38
+ * the payload holds was most of the time of a serialization.
39
+ */
40
+ function createTypeMapper(mapper, model, name) {
41
+ const kind = mapper.constructor;
42
+ const conversion = mapper[name];
43
+ if (kind === PrimitiveMapper &&
44
+ mapper.serialize === PrimitiveMapper.prototype.serialize &&
45
+ isFunction(conversion) &&
46
+ conversion === PrimitiveMapper.prototype[name]) {
47
+ // inline typeof on purpose: this runs for every primitive value, and calling isObject() here was ~15% slower on the serializer benchmark
48
+ return (value) => (typeof value !== "object" ? conversion.call(mapper, value) : value);
49
+ }
50
+ if (kind === DateMapper && mapper.serialize === DateMapper.prototype.serialize) {
51
+ return (value) => mapper.serialize(value);
52
+ }
53
+ return (value, options) => mapper.serialize(value, {
54
+ ...options,
55
+ type: model
56
+ });
57
+ }
22
58
  /**
23
59
  * Serializes data structures according to the metadata captured by `@tsed/schema`.
24
60
  *
@@ -28,6 +64,11 @@ function varKey(k) {
28
64
  export class JsonSerializer extends JsonMapperCompiler {
29
65
  constructor() {
30
66
  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();
31
72
  this.addTypeMapper(Object, this.mapObject.bind(this));
32
73
  this.addTypeMapper(Array, this.mapArray.bind(this));
33
74
  this.addTypeMapper(Map, this.mapMap.bind(this));
@@ -76,15 +117,29 @@ export class JsonSerializer extends JsonMapperCompiler {
76
117
  writer.add(this.mapAdditionalProperties(entity, properties));
77
118
  return writer.return("obj").root().toString();
78
119
  }
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
+ }
79
134
  mapOptions({ groups, useAlias = true, types, ...options }) {
80
135
  const customMappers = {};
81
136
  types = types || getJsonMapperTypes();
82
137
  types.forEach((mapper, model) => {
83
- if (![Array, Set, Map].includes(model)) {
84
- this.addTypeMapper(model, (value, options) => mapper.serialize(value, {
85
- ...options,
86
- type: model
87
- }));
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 });
88
143
  }
89
144
  });
90
145
  const strictGroups = options.strictGroups ?? JsonMapperSettings.strictGroups;
@@ -211,50 +266,60 @@ export class JsonSerializer extends JsonMapperCompiler {
211
266
  if (hasJsonMethod(input)) {
212
267
  return this.mapJSON(input, options);
213
268
  }
214
- return getObjectProperties(input)
215
- .filter(([, value]) => value !== undefined)
216
- .reduce((obj, [key, value]) => {
217
- if (isNil(value)) {
218
- return {
219
- ...obj,
220
- [key]: value
221
- };
269
+ const obj = {};
270
+ // own enumerable keys only, like Object.entries(); functions and undefined values are skipped
271
+ for (const key in input) {
272
+ if (!Object.hasOwn(input, key)) {
273
+ continue;
274
+ }
275
+ const value = input[key];
276
+ if (value === undefined || isFunction(value)) {
277
+ continue;
222
278
  }
223
- const mapper = this.compile(classOf(value), options.groups);
224
- return {
225
- ...obj,
226
- [key]: mapper.fn(value, options)
227
- };
228
- }, {});
279
+ setKey(obj, key, value === null ? value : this.compile(classOf(value), options.groups).fn(value, options));
280
+ }
281
+ return obj;
229
282
  }
230
- mapSet(input, options) {
283
+ // The collection mappers strip `id` from the options once, not once per item:
284
+ // `id` describes the items, it must not leak into the options of a nested object mapper.
285
+ mapSet(input, { id, ...options }) {
231
286
  if (isNil(input)) {
232
287
  return input;
233
288
  }
234
- return [...input.values()].map((item) => {
235
- return this.mapItem(item, options);
236
- });
289
+ const out = [];
290
+ for (const item of input) {
291
+ out.push(this.mapItem(item, id, options));
292
+ }
293
+ return out;
237
294
  }
238
- mapArray(input, options) {
295
+ mapArray(input, { id, ...options }) {
239
296
  if (isNil(input)) {
240
297
  return input;
241
298
  }
242
- return [].concat(input).map((item) => {
243
- return this.mapItem(item, options);
244
- });
299
+ // a single value is mapped as a one-item array, like the former [].concat(input)
300
+ const list = isArray(input) ? input : [input];
301
+ const out = new Array(list.length);
302
+ for (let i = 0; i < list.length; i++) {
303
+ const item = list[i];
304
+ // keep the holes of a sparse array, like the former .map(): the item mapper is not called for them
305
+ if (item === undefined && !(i in list)) {
306
+ continue;
307
+ }
308
+ out[i] = this.mapItem(item, id, options);
309
+ }
310
+ return out;
245
311
  }
246
- mapMap(input, options) {
312
+ mapMap(input, { id, ...options }) {
247
313
  if (isNil(input)) {
248
314
  return input;
249
315
  }
250
- return [...input.entries()].reduce((obj, [key, item]) => {
251
- return {
252
- ...obj,
253
- [key]: this.mapItem(item, options)
254
- };
255
- }, {});
316
+ const obj = {};
317
+ for (const [key, item] of input) {
318
+ setKey(obj, key, this.mapItem(item, id, options));
319
+ }
320
+ return obj;
256
321
  }
257
- mapItem(input, { id, ...options }) {
322
+ mapItem(input, id, options) {
258
323
  if (!id && input) {
259
324
  return this.compile(classOf(input), options.groups).fn(input, options);
260
325
  }
@@ -27,6 +27,12 @@ export declare abstract class JsonMapperCompiler<Options extends Record<string,
27
27
  * @protected
28
28
  */
29
29
  protected cache: Map<string | Type<any>, CachedGroupsJsonMapper<Options>>;
30
+ /**
31
+ * Cached result of `getType()` by model. `getType()` walks the class ancestors
32
+ * and runs for every value serialized without a schema, so it is resolved once per model.
33
+ * @protected
34
+ */
35
+ protected types: Map<Type<any>, Type<any>>;
30
36
  /**
31
37
  * Cached executable mappers by his id
32
38
  * @protected
@@ -67,6 +73,7 @@ export declare abstract class JsonMapperCompiler<Options extends Record<string,
67
73
  protected abstract map(input: any, options: Options): any;
68
74
  protected abstract alterValue(schemaId: string, value: any, options: Options): any;
69
75
  protected abstract createMapper(model: Type<any>, id: string, groups: false | string[]): string;
76
+ protected resolveType(model: Type<any>): Type<any>;
70
77
  protected getType(model: Type<any>): Type<any> | MapConstructor | ObjectConstructor | ArrayConstructor | SetConstructor;
71
78
  protected alterIgnore(id: string, options: Options): any;
72
79
  protected alterGroups(schema: JsonSchema, groups: false | string[]): boolean;
@@ -1,6 +1,14 @@
1
1
  import { Type } from "@tsed/core";
2
- import { JsonMapperCompiler } from "./JsonMapperCompiler.js";
2
+ import { JsonMapperMethods } from "../interfaces/JsonMapperMethods.js";
3
+ import { JsonMapperCompiler, JsonMapperCallback } from "./JsonMapperCompiler.js";
3
4
  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
+ };
4
12
  /**
5
13
  * Serializes data structures according to the metadata captured by `@tsed/schema`.
6
14
  *
@@ -8,10 +16,20 @@ import { JsonSerializerOptions } from "./JsonSerializerOptions.js";
8
16
  * registered type mappers (e.g., components in `src/components`).
9
17
  */
10
18
  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>;
11
24
  constructor();
12
25
  map(input: any, options?: JsonSerializerOptions): any;
13
26
  protected alterValue(schemaId: string, value: any, options: JsonSerializerOptions): any;
14
27
  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;
15
33
  private mapOptions;
16
34
  private mapProperty;
17
35
  private getPropertyFiller;
@@ -26,3 +44,4 @@ export declare class JsonSerializer extends JsonMapperCompiler<JsonSerializerOpt
26
44
  private mapItem;
27
45
  private mapJSON;
28
46
  }
47
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/json-mapper",
3
- "version": "8.44.1",
3
+ "version": "8.44.2",
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.1",
45
- "@tsed/core": "8.44.1",
46
- "@tsed/schema": "8.44.1",
47
- "@tsed/typescript": "8.44.1",
44
+ "@tsed/barrels": "8.44.2",
45
+ "@tsed/core": "8.44.2",
46
+ "@tsed/schema": "8.44.2",
47
+ "@tsed/typescript": "8.44.2",
48
48
  "temporal-polyfill": "1.0.1",
49
49
  "typescript": "6.0.2",
50
50
  "vitest": "4.1.4"