@tsed/json-mapper 8.44.11 → 8.44.12

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.
@@ -39,6 +39,9 @@ function isDeserializable(obj, options) {
39
39
  function varKey(k) {
40
40
  return `__${k}`;
41
41
  }
42
+ function ignoredKey(k) {
43
+ return `__${k}_ignored`;
44
+ }
42
45
  function mapParamStoreOptions(store, options) {
43
46
  return {
44
47
  ...options,
@@ -110,6 +113,7 @@ export class JsonDeserializer extends JsonMapperCompiler {
110
113
  createMapper(model, id, groups) {
111
114
  const entity = s.store(model);
112
115
  const properties = new Set();
116
+ const fallbacks = new Map();
113
117
  const schemaProperties = [...getPropertiesStores(entity).values()];
114
118
  const writer = new Writer().arrow("input", "options");
115
119
  writer.if("isNil(input)").return("input");
@@ -126,17 +130,26 @@ export class JsonDeserializer extends JsonMapperCompiler {
126
130
  // properties
127
131
  writer.add(...schemaProperties.flatMap((propertyStore) => {
128
132
  const key = propertyStore.propertyName;
133
+ const alias = String(propertyStore.parent.schema.getAliasOf(key) || key);
129
134
  // TODO V8 add this line: properties.add(key as string);
130
- properties.add(String(propertyStore.parent.schema.getAliasOf(key) || key));
135
+ properties.add(alias);
131
136
  if ((propertyStore.schema?.$ignore && isBoolean(propertyStore.schema?.$ignore)) ||
132
137
  propertyStore.isGetterOnly() ||
133
138
  (propertyStore.schema?.$hooks?.has("groups") && this.alterGroups(propertyStore.schema, groups))) {
139
+ // an excluded property must not be filled from its declared name either
140
+ properties.add(key);
134
141
  return;
135
142
  }
143
+ // the declared name of an aliased property is still read as an additional property (fallback when the
144
+ // property mapped nothing), but it must not overwrite the value the property mapped
145
+ if (alias !== key) {
146
+ const mappedNothing = `${varKey(key)} === undefined`;
147
+ fallbacks.set(key, propertyStore.schema?.$hooks?.has("ignore") ? `!${ignoredKey(key)} && ${mappedNothing}` : mappedNothing);
148
+ }
136
149
  return this.mapProperty(propertyStore, id, groups);
137
150
  }));
138
151
  // additional properties
139
- writer.add(this.mapAdditionalProperties(entity, properties, groups));
152
+ writer.add(this.mapAdditionalProperties(entity, properties, fallbacks, groups));
140
153
  // post hook
141
154
  if (entity.schema.$hooks?.has("afterDeserialize")) {
142
155
  this.schemes[id] = entity.schema;
@@ -165,10 +178,15 @@ export class JsonDeserializer extends JsonMapperCompiler {
165
178
  const formatOpts = format && `options: {format: '${format}'}`;
166
179
  let writer = new Writer().add(`// Map ${key} ${id} ${groups || ""}`);
167
180
  const pick = key !== aliasKey ? `options.useAlias ? '${aliasKey}' : '${key}'` : `'${key}'`;
181
+ let declare = `let ${varKey(key)}`;
168
182
  // ignore hook (deprecated)
169
183
  if (propertyStore.schema?.$hooks?.has("ignore")) {
170
184
  this.schemes[schemaId] = propertyStore.schema;
171
- writer = writer.if(`!alterIgnore('${schemaId}', {...options, self: input})`);
185
+ // declared outside the branch: the additional properties loop reads them
186
+ writer.add(`${declare};`);
187
+ declare = varKey(key);
188
+ writer.const(ignoredKey(key), `alterIgnore('${schemaId}', {...options, self: input})`);
189
+ writer = writer.if(`!${ignoredKey(key)}`);
172
190
  }
173
191
  // pre hook
174
192
  const hasDeserializer = propertyStore.schema?.$hooks?.has("onDeserialize");
@@ -178,7 +196,7 @@ export class JsonDeserializer extends JsonMapperCompiler {
178
196
  const opts = Writer.options(formatOpts);
179
197
  getter = `alterValue('${schemaId}', input[${pick}], ${opts})`;
180
198
  }
181
- const ifWriter = writer.set(`let ${varKey(key)}`, getter).if(`${varKey(key)} !== undefined`);
199
+ const ifWriter = writer.set(declare, getter).if(`${varKey(key)} !== undefined`);
182
200
  const fill = this.getPropertyFiller(propertyStore, id, groups, formatOpts);
183
201
  if (hasDeserializer) {
184
202
  fill(ifWriter.if(`${varKey(key)} === input.${key}`));
@@ -238,14 +256,29 @@ export class JsonDeserializer extends JsonMapperCompiler {
238
256
  generics: options.generics || {}
239
257
  };
240
258
  }
241
- mapAdditionalProperties(entity, properties, groups) {
259
+ /**
260
+ * @param properties The keys never copied: the keys declared by the model (aliases included), and the declared names of
261
+ * the excluded properties.
262
+ * @param fallbacks The condition to copy the declared name, by declared name of the aliased properties: the property
263
+ * was not ignored and mapped nothing (no input value, or a hook returned `undefined`). A declared name is copied only
264
+ * when the alias is used and this condition is true. The mapped value is checked, not the instance, which may hold a
265
+ * field initializer.
266
+ */
267
+ mapAdditionalProperties(entity, properties, fallbacks, groups) {
242
268
  const additionalProperties = entity.schema.get("additionalProperties");
243
- const exclude = [...properties.values()].map((key) => `'${key}'`).join(", ");
244
269
  const writer = new Writer();
245
270
  writer.add("// add additional properties");
246
- let each = writer.each("objectKeys(input)", ["key"]);
247
- if (exclude.length) {
248
- each = each.if(`![${exclude}].includes(key)`);
271
+ // dynamic additional properties option: the keys are read only when it is enabled, it is disabled by default
272
+ const scope = additionalProperties ? writer : writer.if("options.additionalProperties");
273
+ let each = scope.each("objectKeys(input)", ["key"]);
274
+ if (properties.size) {
275
+ each = each.if(Writer.notOneOf("key", properties));
276
+ }
277
+ if (additionalProperties && fallbacks.size) {
278
+ const conditions = Array.from(fallbacks, ([name, fallback]) => {
279
+ return `(key !== '${name}' || (options.useAlias && ${fallback}))`;
280
+ });
281
+ each = each.if(conditions.join(" && "));
249
282
  }
250
283
  if (additionalProperties && additionalProperties instanceof JsonSchema) {
251
284
  const nestedMapper = this.compile(additionalProperties.class, groups);
@@ -256,8 +289,7 @@ export class JsonDeserializer extends JsonMapperCompiler {
256
289
  each.set("obj[key]", "input[key]");
257
290
  return writer;
258
291
  }
259
- // dynamic additional properties options
260
- each.if(`options.additionalProperties && obj[key] === undefined`).set("obj[key]", "input[key]");
292
+ each.if("obj[key] === undefined").set("obj[key]", "input[key]");
261
293
  return writer;
262
294
  }
263
295
  mapObject(input, options) {
@@ -236,10 +236,9 @@ export class JsonSerializer extends JsonMapperCompiler {
236
236
  mapAdditionalProperties(entity, properties) {
237
237
  const additionalProperties = !!entity.schema.get("additionalProperties");
238
238
  if (additionalProperties) {
239
- const exclude = [...properties.values()].map((key) => `'${key}'`).join(", ");
240
239
  const writer = new Writer();
241
240
  writer.add("// add additional properties");
242
- writer.each("objectKeys(input)", ["key"]).if(`![${exclude}].includes(key)`).set("obj[key]", "input[key]");
241
+ writer.each("objectKeys(input)", ["key"]).if(Writer.notOneOf("key", properties)).set("obj[key]", "input[key]");
243
242
  return writer;
244
243
  }
245
244
  }
@@ -22,6 +22,13 @@ export class Writer {
22
22
  static mapperFrom(key, options) {
23
23
  return `execMapper(nameOf(classOf(${key})), ${key}, ${options})`;
24
24
  }
25
+ /**
26
+ * Condition true when `key` is none of the given values. It compares the key to each value instead of
27
+ * calling `[...].includes(key)`, which allocates an array for every key it checks.
28
+ */
29
+ static notOneOf(key, values) {
30
+ return Array.from(values, (value) => `${key} !== '${value}'`).join(" && ");
31
+ }
25
32
  static options(...args) {
26
33
  args = args.filter(Boolean);
27
34
  return !args.length ? "options" : Writer.object.assign("...options", ...args);
@@ -27,6 +27,14 @@ export declare class JsonDeserializer extends JsonMapperCompiler<JsonDeserialize
27
27
  private mapProperty;
28
28
  private getPropertyFiller;
29
29
  private mapOptions;
30
+ /**
31
+ * @param properties The keys never copied: the keys declared by the model (aliases included), and the declared names of
32
+ * the excluded properties.
33
+ * @param fallbacks The condition to copy the declared name, by declared name of the aliased properties: the property
34
+ * was not ignored and mapped nothing (no input value, or a hook returned `undefined`). A declared name is copied only
35
+ * when the alias is used and this condition is true. The mapped value is checked, not the instance, which may hold a
36
+ * field initializer.
37
+ */
30
38
  private mapAdditionalProperties;
31
39
  private mapObject;
32
40
  private mapSet;
@@ -13,6 +13,11 @@ export declare class Writer {
13
13
  static indent(lines: string[]): string[];
14
14
  static mapper(mapperId: string, key: string, options: string): string;
15
15
  static mapperFrom(key: string, options: string): string;
16
+ /**
17
+ * Condition true when `key` is none of the given values. It compares the key to each value instead of
18
+ * calling `[...].includes(key)`, which allocates an array for every key it checks.
19
+ */
20
+ static notOneOf(key: string, values: Iterable<string>): string;
16
21
  static options(...args: string[]): string;
17
22
  add(...lines: (undefined | string | Writer)[]): this;
18
23
  arrow(...args: string[]): Writer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/json-mapper",
3
- "version": "8.44.11",
3
+ "version": "8.44.12",
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.11",
45
- "@tsed/core": "8.44.11",
46
- "@tsed/schema": "8.44.11",
47
- "@tsed/typescript": "8.44.11",
44
+ "@tsed/barrels": "8.44.12",
45
+ "@tsed/core": "8.44.12",
46
+ "@tsed/schema": "8.44.12",
47
+ "@tsed/typescript": "8.44.12",
48
48
  "temporal-polyfill": "1.0.1",
49
49
  "typescript": "6.0.2",
50
50
  "vitest": "4.1.4"