@tsed/json-mapper 8.0.1 → 8.0.3

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.
Files changed (37) hide show
  1. package/package.json +8 -7
  2. package/readme.md +1 -1
  3. package/src/components/DateMapper.spec.ts +43 -0
  4. package/src/components/DateMapper.ts +29 -0
  5. package/src/components/PrimitiveMapper.spec.ts +179 -0
  6. package/src/components/PrimitiveMapper.ts +64 -0
  7. package/src/components/SymbolMapper.spec.ts +23 -0
  8. package/src/components/SymbolMapper.ts +19 -0
  9. package/src/decorators/afterDeserialize.ts +39 -0
  10. package/src/decorators/beforeDeserialize.ts +39 -0
  11. package/src/decorators/jsonMapper.ts +16 -0
  12. package/src/decorators/onDeserialize.ts +29 -0
  13. package/src/decorators/onSerialize.ts +29 -0
  14. package/src/domain/JsonDeserializer.spec.ts +1437 -0
  15. package/src/domain/JsonDeserializer.ts +402 -0
  16. package/src/domain/JsonDeserializerOptions.ts +31 -0
  17. package/src/domain/JsonMapperCompiler.ts +247 -0
  18. package/src/domain/JsonMapperGlobalOptions.ts +14 -0
  19. package/src/domain/JsonMapperSettings.ts +7 -0
  20. package/src/domain/JsonMapperTypesContainer.ts +20 -0
  21. package/src/domain/JsonSerializer.spec.ts +1505 -0
  22. package/src/domain/JsonSerializer.ts +371 -0
  23. package/src/domain/JsonSerializerOptions.ts +17 -0
  24. package/src/domain/Writer.spec.ts +11 -0
  25. package/src/domain/Writer.ts +181 -0
  26. package/src/domain/__snapshots__/Writer.spec.ts.snap +10 -0
  27. package/src/hooks/alterAfterDeserialize.ts +5 -0
  28. package/src/hooks/alterBeforeDeserialize.ts +5 -0
  29. package/src/hooks/alterOnDeserialize.ts +5 -0
  30. package/src/hooks/alterOnSerialize.ts +5 -0
  31. package/src/index.ts +28 -0
  32. package/src/interfaces/JsonMapperMethods.ts +32 -0
  33. package/src/utils/deserialize.spec.ts +33 -0
  34. package/src/utils/deserialize.ts +12 -0
  35. package/src/utils/getObjectProperties.ts +5 -0
  36. package/src/utils/serialize.spec.ts +37 -0
  37. package/src/utils/serialize.ts +12 -0
@@ -0,0 +1,1505 @@
1
+ import "../components/DateMapper.js";
2
+ import "../components/PrimitiveMapper.js";
3
+ import "../components/SymbolMapper.js";
4
+
5
+ import {cleanObject, isBoolean, isNumber, isObjectID, useDecorators} from "@tsed/core";
6
+ import {
7
+ AdditionalProperties,
8
+ Allow,
9
+ CollectionOf,
10
+ Default,
11
+ DiscriminatorKey,
12
+ DiscriminatorValue,
13
+ Email,
14
+ Groups,
15
+ Ignore,
16
+ JsonHookContext,
17
+ MinLength,
18
+ Name,
19
+ Nullable,
20
+ Property,
21
+ Required,
22
+ Uri
23
+ } from "@tsed/schema";
24
+ import {snakeCase} from "change-case";
25
+ import {parse} from "querystring";
26
+
27
+ import {Post} from "../../test/helpers/Post.js";
28
+ import {User} from "../../test/helpers/User.js";
29
+ import {OnDeserialize} from "../decorators/onDeserialize.js";
30
+ import {OnSerialize} from "../decorators/onSerialize.js";
31
+ import {deserialize} from "../utils/deserialize.js";
32
+ import {JsonMapperSettings} from "./JsonMapperSettings.js";
33
+ import {getJsonMapperTypes} from "./JsonMapperTypesContainer.js";
34
+ import {JsonSerializer} from "./JsonSerializer.js";
35
+
36
+ const serializer = new JsonSerializer();
37
+
38
+ const serialize = (...args: any[]) => (serializer.map as any)(...args);
39
+
40
+ function createMap(value: any) {
41
+ return new Map([["test", value]]);
42
+ }
43
+
44
+ class ObjectId {
45
+ _bsontype = true;
46
+
47
+ constructor(public id: string) {}
48
+
49
+ toString() {
50
+ return this.id;
51
+ }
52
+ }
53
+
54
+ describe("JsonSerializer", () => {
55
+ describe("Primitives", () => {
56
+ it("should serialize values", () => {
57
+ expect(serialize(undefined)).toBeUndefined();
58
+ expect(serialize(null)).toEqual(null);
59
+ expect(serialize("null")).toEqual("null");
60
+ expect(serialize(Symbol.for("TEST"))).toEqual("TEST");
61
+ expect(serialize(false)).toEqual(false);
62
+ expect(serialize(true)).toEqual(true);
63
+ expect(serialize("")).toEqual("");
64
+ expect(serialize("1")).toEqual("1");
65
+ expect(serialize(0)).toEqual(0);
66
+ expect(serialize(1)).toEqual(1);
67
+ expect(serialize(1)).toEqual(1);
68
+ expect(serialize(BigInt(1n))).toEqual(BigInt(1));
69
+ });
70
+ });
71
+ describe("Object", () => {
72
+ it("should serialize values", () => {
73
+ expect(serialize({test: "test"}, {type: Object})).toEqual({test: "test"});
74
+ expect(serialize({test: "test"}, {type: false})).toEqual({test: "test"});
75
+ expect(serialize({test: "test"}, {type: null})).toEqual({test: "test"});
76
+ expect(serialize({test: "test"}, {type: undefined})).toEqual({test: "test"});
77
+ });
78
+ it("should serialize parsed querystring", () => {
79
+ expect(serialize({qs: parse("q[offset]=0&q[limit]=10&q[where][a]=0&q[where][b]=1")})).toEqual({
80
+ qs: {
81
+ "q[limit]": "10",
82
+ "q[offset]": "0",
83
+ "q[where][a]": "0",
84
+ "q[where][b]": "1"
85
+ }
86
+ });
87
+ });
88
+ });
89
+ describe("Array<primitive>", () => {
90
+ it("should serialize values", () => {
91
+ expect(serialize([null])).toEqual([null]);
92
+ expect(serialize([false])).toEqual([false]);
93
+ expect(serialize([true])).toEqual([true]);
94
+ expect(serialize([""])).toEqual([""]);
95
+ expect(serialize(["1"])).toEqual(["1"]);
96
+ expect(serialize([1])).toEqual([1]);
97
+
98
+ class ArrayLike extends Array {}
99
+
100
+ const arrayLike = new ArrayLike();
101
+ arrayLike.push(1);
102
+
103
+ expect(serialize(arrayLike)).toEqual([1]);
104
+
105
+ class SetLike extends Set {}
106
+
107
+ const setLike = new SetLike();
108
+ setLike.add(1);
109
+
110
+ expect(serialize(setLike)).toEqual([1]);
111
+
112
+ class MapLike extends Map {}
113
+
114
+ const mapLike = new MapLike();
115
+ mapLike.set("i", 1);
116
+
117
+ expect(serialize(mapLike)).toEqual({i: 1});
118
+ });
119
+ });
120
+ describe("Map<primitive>", () => {
121
+ it("should serialize values", () => {
122
+ expect(serialize(createMap(null))).toEqual({test: null});
123
+ expect(serialize(createMap(false))).toEqual({test: false});
124
+ expect(serialize(createMap(true))).toEqual({test: true});
125
+ expect(serialize(createMap(""))).toEqual({test: ""});
126
+ expect(serialize(createMap("1"))).toEqual({test: "1"});
127
+ expect(serialize(createMap(1))).toEqual({test: 1});
128
+ });
129
+ });
130
+ describe("toJson()", () => {
131
+ it("should serialize obj from toJSON", () => {
132
+ const result = serialize({
133
+ toJSON() {
134
+ return "hello";
135
+ }
136
+ });
137
+
138
+ expect(result).toEqual("hello");
139
+ });
140
+
141
+ it("should serialize obj from toJSON (with type)", () => {
142
+ class Model {}
143
+
144
+ const result = serialize(
145
+ {
146
+ toJSON() {
147
+ return "hello";
148
+ }
149
+ },
150
+ {type: Model}
151
+ );
152
+
153
+ expect(result).toEqual("hello");
154
+ });
155
+ });
156
+ describe("Plain Object", () => {
157
+ it("should serialize plain object (1)", () => {
158
+ expect(serialize({prop: "1"})).toEqual({prop: "1"});
159
+
160
+ const result = serialize({
161
+ prop: "1",
162
+ roles: [{label: "Admin"}]
163
+ });
164
+ expect(result).toEqual({
165
+ prop: "1",
166
+ roles: [
167
+ {
168
+ label: "Admin"
169
+ }
170
+ ]
171
+ });
172
+ });
173
+ it("should serialize plain object (2)", () => {
174
+ expect(serialize({prop: "1"})).toEqual({prop: "1"});
175
+ const roles = new Map();
176
+ roles.set("ro", "le");
177
+
178
+ const result = serialize({
179
+ prop: "1",
180
+ roles
181
+ });
182
+ expect(result).toEqual({
183
+ prop: "1",
184
+ roles: {
185
+ ro: "le"
186
+ }
187
+ });
188
+ });
189
+ });
190
+ describe("Class", () => {
191
+ describe("ignore hook is configured on props", () => {
192
+ it("should serialize model (api = true)", () => {
193
+ class Role {
194
+ @Property()
195
+ label: string;
196
+
197
+ constructor({label}: any = {}) {
198
+ this.label = label;
199
+ }
200
+ }
201
+
202
+ class Model {
203
+ @Property()
204
+ id: string;
205
+
206
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
207
+ password: string;
208
+
209
+ @OnSerialize((value) => String(value) + "test")
210
+ @Name("mapped_prop")
211
+ mappedProp: string;
212
+
213
+ @CollectionOf(Role)
214
+ roles: Role[] = [];
215
+
216
+ @CollectionOf(Role)
217
+ mapRoles: Map<string, Role> = new Map();
218
+
219
+ @CollectionOf(String)
220
+ setRoleNames: Set<string> = new Set();
221
+ }
222
+
223
+ const model = new Model();
224
+ model.id = "id";
225
+ model.password = "string";
226
+ model.mappedProp = "mappedProp";
227
+ model.roles = [new Role({label: "ADMIN"})];
228
+
229
+ model.mapRoles = new Map([["ADMIN", new Role({label: "ADMIN"})]]);
230
+
231
+ model.setRoleNames = new Set();
232
+ model.setRoleNames.add("ADMIN");
233
+
234
+ expect(
235
+ serialize(model, {
236
+ useAlias: false,
237
+ api: true
238
+ })
239
+ ).toEqual({
240
+ id: "id",
241
+ mapRoles: {
242
+ ADMIN: {
243
+ label: "ADMIN"
244
+ }
245
+ },
246
+ mappedProp: "mappedProptest",
247
+ roles: [
248
+ {
249
+ label: "ADMIN"
250
+ }
251
+ ],
252
+ setRoleNames: ["ADMIN"]
253
+ });
254
+ });
255
+ it("should serialize model (api = false)", () => {
256
+ class Role {
257
+ @Property()
258
+ label: string;
259
+
260
+ constructor({label}: any = {}) {
261
+ this.label = label;
262
+ }
263
+ }
264
+
265
+ class Model {
266
+ @Property()
267
+ id: string;
268
+
269
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
270
+ password: string;
271
+
272
+ @OnSerialize((value) => String(value) + "test")
273
+ @Name("mapped_prop")
274
+ mappedProp: string;
275
+
276
+ @CollectionOf(Role)
277
+ roles: Role[] = [];
278
+
279
+ @CollectionOf(Role)
280
+ mapRoles: Map<string, Role> = new Map();
281
+
282
+ @CollectionOf(String)
283
+ setRoleNames: Set<string> = new Set();
284
+ }
285
+
286
+ const model = new Model();
287
+ model.id = "id";
288
+ model.password = "string";
289
+ model.mappedProp = "mappedProp";
290
+ model.roles = [new Role({label: "ADMIN"})];
291
+
292
+ model.mapRoles = new Map([["ADMIN", new Role({label: "ADMIN"})]]);
293
+
294
+ model.setRoleNames = new Set();
295
+ model.setRoleNames.add("ADMIN");
296
+
297
+ expect(
298
+ serialize(model, {
299
+ useAlias: false,
300
+ api: false
301
+ })
302
+ ).toEqual({
303
+ id: "id",
304
+ mapRoles: {
305
+ ADMIN: {
306
+ label: "ADMIN"
307
+ }
308
+ },
309
+ password: "string",
310
+ mappedProp: "mappedProptest",
311
+ roles: [
312
+ {
313
+ label: "ADMIN"
314
+ }
315
+ ],
316
+ setRoleNames: ["ADMIN"]
317
+ });
318
+ });
319
+ });
320
+ it("should serialize model and take the right type", () => {
321
+ class Role {
322
+ @Property()
323
+ label: string;
324
+
325
+ constructor({label}: any = {}) {
326
+ this.label = label;
327
+ }
328
+ }
329
+
330
+ class Model {
331
+ @Property()
332
+ id: string;
333
+
334
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
335
+ password: string;
336
+
337
+ @OnSerialize((value) => String(value) + "test")
338
+ @Name("mapped_prop")
339
+ mappedProp: string;
340
+
341
+ @CollectionOf(Role)
342
+ roles: Map<string, Role> = new Map();
343
+ }
344
+
345
+ class ServerResponse {
346
+ @Property()
347
+ data: Model;
348
+ }
349
+
350
+ const model = new Model();
351
+ model.id = "id";
352
+ model.password = "hellopassword";
353
+ model.mappedProp = "hello";
354
+ model.roles.set("olo", new Role({label: "label"}));
355
+
356
+ expect(serialize(model, {type: ServerResponse})).toEqual({
357
+ id: "id",
358
+ password: "hellopassword",
359
+ mapped_prop: "hellotest",
360
+ roles: {
361
+ olo: {
362
+ label: "label"
363
+ }
364
+ }
365
+ });
366
+ });
367
+ it("should serialize model with alias property", () => {
368
+ class Role {
369
+ @Property()
370
+ label: string;
371
+
372
+ constructor({label}: any = {}) {
373
+ this.label = label;
374
+ }
375
+ }
376
+
377
+ class Model {
378
+ @Property()
379
+ id: string;
380
+
381
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
382
+ password: string;
383
+
384
+ @OnSerialize((value) => String(value) + "test")
385
+ @Name("mapped_prop")
386
+ mappedProp: string;
387
+
388
+ @CollectionOf(Role)
389
+ roles: Role[] = [];
390
+
391
+ @CollectionOf(Role)
392
+ mapRoles: Map<string, Role> = new Map();
393
+
394
+ @CollectionOf(String)
395
+ setRoleNames: Set<string> = new Set();
396
+ }
397
+
398
+ const model = new Model();
399
+ model.id = "id";
400
+ model.password = "string";
401
+ model.mappedProp = "mappedProp";
402
+ model.roles = [new Role({label: "ADMIN"})];
403
+
404
+ model.mapRoles = new Map([["ADMIN", new Role({label: "ADMIN"})]]);
405
+
406
+ model.setRoleNames = new Set();
407
+ model.setRoleNames.add("ADMIN");
408
+
409
+ expect(
410
+ serialize(model, {
411
+ useAlias: true,
412
+ api: false
413
+ })
414
+ ).toEqual({
415
+ id: "id",
416
+ mapRoles: {
417
+ ADMIN: {
418
+ label: "ADMIN"
419
+ }
420
+ },
421
+ mapped_prop: "mappedProptest",
422
+ password: "string",
423
+ roles: [
424
+ {
425
+ label: "ADMIN"
426
+ }
427
+ ],
428
+ setRoleNames: ["ADMIN"]
429
+ });
430
+ });
431
+ it("should serialize model with nested object", () => {
432
+ class Nested {
433
+ @Property()
434
+ label: string;
435
+
436
+ @Name("additional_description")
437
+ additionalDescription: string;
438
+
439
+ constructor({label, additionalDescription}: any = {}) {
440
+ this.label = label;
441
+ this.additionalDescription = additionalDescription;
442
+ }
443
+ }
444
+
445
+ class Model {
446
+ @Property()
447
+ id: string;
448
+
449
+ @Property()
450
+ nested: any;
451
+
452
+ @Property()
453
+ nestedTyped: Nested;
454
+ }
455
+
456
+ const model = new Model();
457
+ model.id = "id";
458
+ model.nested = {
459
+ other: "other",
460
+ test: new Nested({
461
+ additionalDescription: "additionalDescription",
462
+ label: "label"
463
+ })
464
+ };
465
+
466
+ model.nestedTyped = new Nested({
467
+ additionalDescription: "additionalDescription",
468
+ label: "label"
469
+ });
470
+
471
+ expect(
472
+ serialize(model, {
473
+ useAlias: true,
474
+ api: false
475
+ })
476
+ ).toEqual({
477
+ id: "id",
478
+ nested: {
479
+ other: "other",
480
+ test: {
481
+ additional_description: "additionalDescription",
482
+ label: "label"
483
+ }
484
+ },
485
+ nestedTyped: {
486
+ additional_description: "additionalDescription",
487
+ label: "label"
488
+ }
489
+ });
490
+ });
491
+ it("should serialize model with additional properties", () => {
492
+ @AdditionalProperties(true)
493
+ class Model {
494
+ @Property()
495
+ id: string;
496
+
497
+ @OnSerialize((value) => String(value) + "test")
498
+ @Name("mapped_prop")
499
+ mappedProp: string;
500
+ }
501
+
502
+ expect(
503
+ serialize(
504
+ {
505
+ id: "id",
506
+ mappedProp: "mappedProp",
507
+ additionalProperty: true
508
+ },
509
+ {
510
+ type: Model,
511
+ useAlias: false
512
+ }
513
+ )
514
+ ).toEqual({
515
+ additionalProperty: true,
516
+ id: "id",
517
+ mappedProp: "mappedProptest"
518
+ });
519
+ });
520
+ it("should serialize model (recursive class)", () => {
521
+ class User {
522
+ @Property()
523
+ name: string;
524
+
525
+ @CollectionOf(() => Post)
526
+ posts: any[];
527
+
528
+ @Property(() => Post)
529
+ @Name("main_post")
530
+ mainPost: any;
531
+ }
532
+
533
+ class Post {
534
+ @Property()
535
+ id: string;
536
+
537
+ @Property()
538
+ owner: User;
539
+
540
+ @Name("title")
541
+ initializedTitle: string;
542
+ }
543
+
544
+ const post = new Post();
545
+ post.id = "id";
546
+
547
+ post.owner = new User();
548
+ post.owner.name = "name";
549
+ post.owner.posts = [new Post()];
550
+ post.owner.posts[0].id = "id";
551
+ post.owner.posts[0].initializedTitle = "initializedTitle";
552
+
553
+ post.owner.mainPost = new Post();
554
+ post.owner.mainPost.id = "idMain";
555
+ post.owner.mainPost.initializedTitle = "initializedTitle";
556
+
557
+ const result = serialize(post, {useAlias: true});
558
+
559
+ expect(result).toEqual({
560
+ id: "id",
561
+ owner: {
562
+ name: "name",
563
+ main_post: {
564
+ id: "idMain",
565
+ title: "initializedTitle"
566
+ },
567
+ posts: [
568
+ {
569
+ id: "id",
570
+ title: "initializedTitle"
571
+ }
572
+ ]
573
+ }
574
+ });
575
+ });
576
+ it("should serialize model without props", () => {
577
+ class Test {
578
+ raw: any;
579
+ affected?: number | null;
580
+ }
581
+
582
+ const t = new Test();
583
+
584
+ t.raw = 1;
585
+ t.affected = 1;
586
+
587
+ const result = serialize(t, {type: Object});
588
+
589
+ expect(result).toEqual({
590
+ affected: 1,
591
+ raw: 1
592
+ });
593
+ });
594
+ it("should discover property dynamically when any schema decorators are used", () => {
595
+ function log(): PropertyDecorator {
596
+ return () => {};
597
+ }
598
+
599
+ class User {
600
+ @log()
601
+ id: string;
602
+
603
+ test: string;
604
+
605
+ [key: string]: any;
606
+
607
+ constructor({id, test}: any = {}) {
608
+ this.id = id;
609
+ test && (this.test = test);
610
+ }
611
+ }
612
+
613
+ const user1 = new User({id: "id"});
614
+ const user2 = new User({id: "id", test: "test"});
615
+ const user3 = new User({id: "id", test: "test"});
616
+
617
+ user3.extra = "extra";
618
+
619
+ expect(serialize(user1, {type: User})).toEqual({id: "id"});
620
+ expect(serialize(user2, {type: User})).toEqual({id: "id", test: "test"});
621
+ expect(serialize(user3, {type: User})).toEqual({
622
+ extra: "extra",
623
+ id: "id",
624
+ test: "test"
625
+ });
626
+ });
627
+ it("should serialize model with null values", () => {
628
+ class NestedModel {
629
+ @Property()
630
+ id: string;
631
+ }
632
+
633
+ class NullModel {
634
+ @Property()
635
+ prop1: string;
636
+
637
+ @Property()
638
+ prop2: number;
639
+
640
+ @Property()
641
+ prop3: Date;
642
+
643
+ @Nullable(NestedModel)
644
+ prop4: NestedModel;
645
+ }
646
+
647
+ expect(
648
+ serialize(
649
+ {
650
+ prop1: null,
651
+ prop2: null,
652
+ prop3: null,
653
+ prop4: null
654
+ },
655
+ {type: NullModel}
656
+ )
657
+ ).toEqual({
658
+ prop1: null,
659
+ prop2: null,
660
+ prop3: null,
661
+ prop4: null
662
+ });
663
+ });
664
+ it("should transform keep array, map, set without nullable value", () => {
665
+ class Model {
666
+ @CollectionOf(String)
667
+ public bars?: string[];
668
+
669
+ @CollectionOf(String)
670
+ public barsSet?: Set<string>;
671
+
672
+ @CollectionOf(String)
673
+ public barsMap?: Map<string, string>;
674
+ }
675
+
676
+ const payload = {
677
+ bars: null,
678
+ barsSet: null,
679
+ barsMap: null
680
+ };
681
+
682
+ const result = serialize(payload, {type: Model});
683
+
684
+ expect(result).toEqual({
685
+ bars: null,
686
+ barsSet: null,
687
+ barsMap: null
688
+ });
689
+ });
690
+ it("should serialize array model with alias property", () => {
691
+ class SpaBooking {
692
+ @Required()
693
+ @Name("booking_number")
694
+ bookingNumber: string;
695
+
696
+ @Required()
697
+ status: string;
698
+
699
+ @Required()
700
+ @Name("order_id")
701
+ orderId: number;
702
+
703
+ @Required()
704
+ @Name("appointment_id")
705
+ appointmentId: number;
706
+
707
+ @Name("customer_id")
708
+ @Groups("!create", "!read")
709
+ customerId: number;
710
+ }
711
+
712
+ const appointments = [
713
+ {
714
+ bookingNumber: "100566224434",
715
+ status: "Booked",
716
+ orderId: 711376505,
717
+ appointmentId: 566224434,
718
+ customerId: null
719
+ }
720
+ ];
721
+
722
+ const serializedResult = serialize(appointments, {
723
+ type: SpaBooking
724
+ });
725
+
726
+ expect(serializedResult).toEqual([
727
+ {
728
+ booking_number: "100566224434",
729
+ status: "Booked",
730
+ order_id: 711376505,
731
+ appointment_id: 566224434,
732
+ customer_id: null
733
+ }
734
+ ]);
735
+ });
736
+ it("should serialize model without decorator", () => {
737
+ class Client {
738
+ clientId: string;
739
+ clientSecret: string;
740
+ }
741
+
742
+ const client = new Client();
743
+ client.clientId = "id";
744
+ client.clientSecret = "secret";
745
+
746
+ const result = serialize(client, {useAlias: false, groups: ["render"]});
747
+
748
+ expect(result === client).toBeFalsy();
749
+ expect(result).toEqual({
750
+ clientId: "id",
751
+ clientSecret: "secret"
752
+ });
753
+ });
754
+ });
755
+ describe("class with toJSON/toClass", () => {
756
+ it("should serialize model", () => {
757
+ class Role {
758
+ @Property()
759
+ label: string;
760
+
761
+ constructor({label}: any = {}) {
762
+ this.label = label;
763
+ }
764
+ }
765
+
766
+ class Model {
767
+ @Property()
768
+ id: string;
769
+
770
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
771
+ password: string;
772
+
773
+ @OnSerialize((value) => String(value) + "test")
774
+ @Name("mapped_prop")
775
+ mappedProp: string;
776
+
777
+ @CollectionOf(Role)
778
+ roles: Map<string, Role> = new Map();
779
+ }
780
+
781
+ const model = new Model();
782
+ // @ts-ignore
783
+ model.$isMongooseModelPrototype = true;
784
+ // @ts-ignore
785
+ model["toJSON"] = (options: any) => {
786
+ const result = deserialize(
787
+ {
788
+ id: "id",
789
+ password: "hellopassword",
790
+ mappedProp: "hello"
791
+ },
792
+ {useAlias: false, type: Model}
793
+ );
794
+
795
+ return serialize(result, options);
796
+ };
797
+
798
+ expect(serialize(model, {type: Model})).toEqual({
799
+ id: "id",
800
+ mapped_prop: "hellotest",
801
+ password: "hellopassword",
802
+ roles: {}
803
+ });
804
+
805
+ expect(serialize(model, {api: true, useAlias: false})).toEqual({
806
+ id: "id",
807
+ mappedProp: "hellotest",
808
+ roles: {}
809
+ });
810
+
811
+ expect(serialize([model], {type: Model})).toEqual([
812
+ {
813
+ id: "id",
814
+ mapped_prop: "hellotest",
815
+ password: "hellopassword",
816
+ roles: {}
817
+ }
818
+ ]);
819
+ });
820
+ it("should serialize model (protected keyword)", () => {
821
+ class Role {
822
+ @Property()
823
+ label: string;
824
+
825
+ constructor({label}: any = {}) {
826
+ this.label = label;
827
+ }
828
+ }
829
+
830
+ class Model {
831
+ @Property()
832
+ id: string;
833
+
834
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
835
+ password: string;
836
+
837
+ @OnSerialize((value) => String(value) + "test")
838
+ @Name("mapped_prop")
839
+ mappedProp: string;
840
+
841
+ @Property()
842
+ enum: string;
843
+
844
+ @CollectionOf(Role)
845
+ roles: Map<string, Role> = new Map();
846
+ }
847
+
848
+ const model = new Model();
849
+ // @ts-ignore
850
+ model.$isMongooseModelPrototype = true;
851
+ // @ts-ignore
852
+ model["toJSON"] = (options: any) => {
853
+ const result = deserialize(
854
+ {
855
+ id: "id",
856
+ password: "hellopassword",
857
+ mappedProp: "hello",
858
+ enum: "test"
859
+ },
860
+ {useAlias: false, type: Model}
861
+ );
862
+
863
+ return serialize(result, options);
864
+ };
865
+
866
+ expect(serialize(model, {type: Model})).toEqual({
867
+ id: "id",
868
+ enum: "test",
869
+ mapped_prop: "hellotest",
870
+ password: "hellopassword",
871
+ roles: {}
872
+ });
873
+
874
+ expect(serialize(model, {api: true, useAlias: false})).toEqual({
875
+ id: "id",
876
+ enum: "test",
877
+ mappedProp: "hellotest",
878
+ roles: {}
879
+ });
880
+
881
+ expect(serialize([model], {type: Model})).toEqual([
882
+ {
883
+ id: "id",
884
+ enum: "test",
885
+ mapped_prop: "hellotest",
886
+ password: "hellopassword",
887
+ roles: {}
888
+ }
889
+ ]);
890
+ });
891
+ it("should serialize model Array", () => {
892
+ class Role {
893
+ @Property()
894
+ label: string;
895
+
896
+ constructor({label}: any = {}) {
897
+ this.label = label;
898
+ }
899
+ }
900
+
901
+ class Model {
902
+ @Property()
903
+ id: string;
904
+
905
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
906
+ password: string;
907
+
908
+ @OnSerialize((value) => String(value) + "test")
909
+ @Name("mapped_prop")
910
+ mappedProp: string;
911
+
912
+ @CollectionOf(Role)
913
+ roles: Map<string, Role> = new Map();
914
+ }
915
+
916
+ const model = new Model();
917
+ model.id = "id";
918
+ model.password = "hellopassword";
919
+ model.mappedProp = "hello";
920
+ model.roles.set("olo", new Role({label: "label"}));
921
+
922
+ expect(serialize([model], {type: Model})).toEqual([
923
+ {
924
+ id: "id",
925
+ password: "hellopassword",
926
+ mapped_prop: "hellotest",
927
+ roles: {
928
+ olo: {
929
+ label: "label"
930
+ }
931
+ }
932
+ }
933
+ ]);
934
+ });
935
+ it("should serialize model with nested model and not populated data (mongoose)", () => {
936
+ class Workspace {
937
+ @Property()
938
+ _id: string;
939
+
940
+ @Property()
941
+ name: string;
942
+ }
943
+
944
+ class MyWorkspace {
945
+ @Property()
946
+ workspaceId: Workspace;
947
+
948
+ @Property()
949
+ title: string;
950
+ }
951
+
952
+ class UserWorkspace {
953
+ @Property()
954
+ _id: string;
955
+
956
+ @CollectionOf(MyWorkspace)
957
+ workspaces: MyWorkspace[];
958
+ }
959
+
960
+ const userWorkspace = new UserWorkspace();
961
+ userWorkspace._id = new ObjectId("64e061ba7356daf00a66c130") as unknown as string;
962
+ userWorkspace.workspaces = [new MyWorkspace()];
963
+ userWorkspace.workspaces[0].title = "MyTest";
964
+ userWorkspace.workspaces[0].workspaceId = new ObjectId("64e061ba7356daf00a66c130") as unknown as Workspace;
965
+
966
+ expect(serialize(userWorkspace, {type: UserWorkspace})).toEqual({
967
+ _id: "64e061ba7356daf00a66c130",
968
+ workspaces: [
969
+ {
970
+ title: "MyTest",
971
+ workspaceId: "64e061ba7356daf00a66c130"
972
+ }
973
+ ]
974
+ });
975
+ });
976
+ it("should serialize model with nested model and not populated data (Ref mongoose)", () => {
977
+ class TestUser {
978
+ @Required()
979
+ email: string;
980
+
981
+ @Required()
982
+ @MinLength(6)
983
+ @Groups("creation")
984
+ password: string;
985
+ }
986
+
987
+ class TestProfile {
988
+ @OnSerialize((value, ctx) => {
989
+ if (isObjectID(value)) {
990
+ return value.toString();
991
+ }
992
+
993
+ return serialize(value, {...ctx, type: TestUser});
994
+ })
995
+ user: any;
996
+ }
997
+
998
+ const profile = new TestProfile();
999
+ profile.user = new ObjectId("64e061ba7356daf00a66c130");
1000
+
1001
+ expect(serialize([profile])).toEqual([
1002
+ {
1003
+ user: "64e061ba7356daf00a66c130"
1004
+ }
1005
+ ]);
1006
+ });
1007
+ it("should serialize model (inherited class)", () => {
1008
+ class Role {
1009
+ @Property()
1010
+ label: string;
1011
+
1012
+ constructor({label}: any = {}) {
1013
+ this.label = label;
1014
+ }
1015
+ }
1016
+
1017
+ class Base {
1018
+ @CollectionOf(Role)
1019
+ roles: Map<string, Role> = new Map();
1020
+ }
1021
+
1022
+ class Model extends Base {
1023
+ @Property()
1024
+ id: string;
1025
+
1026
+ @Ignore((ignored, ctx: JsonHookContext) => ctx.api)
1027
+ password: string;
1028
+
1029
+ @OnSerialize((value) => String(value) + "test")
1030
+ @Name("mapped_prop")
1031
+ mappedProp: string;
1032
+ }
1033
+
1034
+ const model = new Model();
1035
+ model.id = "id";
1036
+ model.password = "hellopassword";
1037
+ model.mappedProp = "hello";
1038
+ model.roles.set("olo", new Role({label: "label"}));
1039
+
1040
+ expect(serialize(model)).toEqual({
1041
+ id: "id",
1042
+ password: "hellopassword",
1043
+ mapped_prop: "hellotest",
1044
+ roles: {
1045
+ olo: {
1046
+ label: "label"
1047
+ }
1048
+ }
1049
+ });
1050
+
1051
+ expect(serialize(model, {api: true, useAlias: false})).toEqual({
1052
+ id: "id",
1053
+ mappedProp: "hellotest",
1054
+ roles: {
1055
+ olo: {
1056
+ label: "label"
1057
+ }
1058
+ }
1059
+ });
1060
+ });
1061
+ it("should serialize model (recursive class)", () => {
1062
+ const post = new Post();
1063
+ post.id = "id";
1064
+ post.owner = new User();
1065
+ post.owner.name = "name";
1066
+ post.owner.posts = [new Post()];
1067
+ post.owner.posts[0].id = "id";
1068
+
1069
+ const result = serialize(post);
1070
+
1071
+ expect(result).toEqual({
1072
+ id: "id",
1073
+ owner: {
1074
+ name: "name",
1075
+ posts: [
1076
+ {
1077
+ id: "id"
1078
+ }
1079
+ ]
1080
+ }
1081
+ });
1082
+ });
1083
+ it("should serialize model with object props", () => {
1084
+ class Model {
1085
+ @Property()
1086
+ test: any;
1087
+
1088
+ @Property()
1089
+ id: string;
1090
+ }
1091
+
1092
+ const test = new Model();
1093
+ test.id = "id";
1094
+ test.test = {
1095
+ value: "test",
1096
+ nullProp: null
1097
+ };
1098
+
1099
+ const result = serialize(test, {type: Model});
1100
+
1101
+ expect(result).toEqual({
1102
+ id: "id",
1103
+ test: {
1104
+ value: "test",
1105
+ nullProp: null
1106
+ }
1107
+ });
1108
+ });
1109
+ it("should serialize model with additional object props", () => {
1110
+ @AdditionalProperties(true)
1111
+ class Model {
1112
+ @Property()
1113
+ id: string;
1114
+
1115
+ @Ignore(true)
1116
+ ignored: boolean;
1117
+
1118
+ @Name("renamed")
1119
+ name: string;
1120
+
1121
+ [type: string]: any;
1122
+ }
1123
+
1124
+ const test = new Model();
1125
+ test.id = "id";
1126
+ test.ignored = true;
1127
+ test.name = "myname";
1128
+ test.additional = {foo: "bar"};
1129
+
1130
+ const result = serialize(test, {type: Model});
1131
+
1132
+ expect(result).toEqual({
1133
+ id: "id",
1134
+ renamed: "myname",
1135
+ additional: {
1136
+ foo: "bar"
1137
+ }
1138
+ });
1139
+ });
1140
+ it("should not serialize model with additional object props", () => {
1141
+ class Model {
1142
+ @Property()
1143
+ id: string;
1144
+
1145
+ @Ignore(true)
1146
+ ignored: boolean;
1147
+
1148
+ @Name("renamed")
1149
+ name: string;
1150
+
1151
+ [type: string]: any;
1152
+ }
1153
+
1154
+ const test = new Model();
1155
+ test.id = "id";
1156
+ test.ignored = true;
1157
+ test.name = "myname";
1158
+ test.additional = {foo: "bar"};
1159
+
1160
+ const result = serialize(test, {type: Model});
1161
+
1162
+ expect(result).toEqual({
1163
+ id: "id",
1164
+ renamed: "myname"
1165
+ });
1166
+ });
1167
+ it("should serialize model to object (with default value - no assigned)", () => {
1168
+ class SpaCareCategory {
1169
+ @Required()
1170
+ @Groups("!details")
1171
+ id: string;
1172
+
1173
+ @Required()
1174
+ label: string;
1175
+
1176
+ @Required()
1177
+ @OnDeserialize((name: string) => snakeCase(name).toUpperCase())
1178
+ code: string;
1179
+
1180
+ @Required()
1181
+ @Default(0)
1182
+ @OnSerialize((o) => {
1183
+ return o || 0;
1184
+ })
1185
+ weight: number = 0;
1186
+
1187
+ constructor({id, label, code, weight}: Partial<SpaCareCategory> = {}) {
1188
+ Object.assign(
1189
+ this,
1190
+ cleanObject({
1191
+ id,
1192
+ label,
1193
+ code,
1194
+ weight
1195
+ })
1196
+ );
1197
+ }
1198
+ }
1199
+
1200
+ expect(
1201
+ serialize(
1202
+ {
1203
+ label: "categoryLabel",
1204
+ code: "CATEGORY_CODE"
1205
+ },
1206
+ {type: SpaCareCategory}
1207
+ )
1208
+ ).toEqual({
1209
+ code: "CATEGORY_CODE",
1210
+ label: "categoryLabel",
1211
+ weight: 0
1212
+ });
1213
+ });
1214
+ it("should serialize model to object (with default value - no assigned - custom decorator)", () => {
1215
+ function AllowEmpty() {
1216
+ return useDecorators(
1217
+ Default(""),
1218
+ Allow(""),
1219
+ OnDeserialize((o: any) => (o === null || o === undefined ? "" : o)),
1220
+ OnSerialize((o: any) => (o === null || o === undefined ? "" : o))
1221
+ );
1222
+ }
1223
+
1224
+ class SpaInformation {
1225
+ @Required()
1226
+ id: number;
1227
+
1228
+ @Required()
1229
+ @AllowEmpty()
1230
+ label: string = "";
1231
+
1232
+ @Required()
1233
+ @AllowEmpty()
1234
+ description: string = "";
1235
+
1236
+ @AllowEmpty()
1237
+ currency: string = "EUR";
1238
+
1239
+ @Email()
1240
+ @AllowEmpty()
1241
+ email: string;
1242
+
1243
+ @AllowEmpty()
1244
+ phone: string;
1245
+
1246
+ @Required()
1247
+ @Name("are_children_accepted")
1248
+ areChildrenAccepted: boolean = false;
1249
+
1250
+ @AllowEmpty()
1251
+ website: string = "";
1252
+
1253
+ @Uri()
1254
+ @AllowEmpty()
1255
+ logo: string;
1256
+
1257
+ @Uri()
1258
+ @AllowEmpty()
1259
+ image: string;
1260
+
1261
+ @AllowEmpty()
1262
+ location: string = "";
1263
+
1264
+ @Name("cancellation_hours_limit")
1265
+ @Nullable(Number)
1266
+ @OnSerialize((o) => (isNumber(o) ? o : null))
1267
+ cancellationHoursLimit: number | null = null;
1268
+
1269
+ constructor({
1270
+ id,
1271
+ label,
1272
+ description,
1273
+ currency,
1274
+ email,
1275
+ phone,
1276
+ areChildrenAccepted,
1277
+ website,
1278
+ logo,
1279
+ image,
1280
+ location,
1281
+ cancellationHoursLimit = null
1282
+ }: Partial<SpaInformation> = {}) {
1283
+ Object.assign(
1284
+ this,
1285
+ cleanObject({
1286
+ id,
1287
+ label,
1288
+ description,
1289
+ currency,
1290
+ email,
1291
+ phone,
1292
+ areChildrenAccepted,
1293
+ website,
1294
+ logo,
1295
+ image,
1296
+ location,
1297
+ cancellationHoursLimit
1298
+ })
1299
+ );
1300
+ }
1301
+ }
1302
+
1303
+ const result = serialize(
1304
+ {
1305
+ id: 453,
1306
+ address: null,
1307
+ label: null,
1308
+ currency: null,
1309
+ description: null,
1310
+ email: null,
1311
+ phone: undefined,
1312
+ areChildrenAccepted: true,
1313
+ website: "website",
1314
+ logo: undefined,
1315
+ image: null,
1316
+ cares: [],
1317
+ cancellationHoursLimit: undefined
1318
+ },
1319
+ {type: SpaInformation}
1320
+ );
1321
+
1322
+ expect(result).toEqual({
1323
+ are_children_accepted: true,
1324
+ currency: "",
1325
+ description: "",
1326
+ email: "",
1327
+ id: 453,
1328
+ image: "",
1329
+ label: "",
1330
+ location: "",
1331
+ logo: "",
1332
+ phone: "",
1333
+ website: "website",
1334
+ cancellation_hours_limit: null
1335
+ });
1336
+ });
1337
+
1338
+ describe("when jsonMapper.strictGroups = false", () => {
1339
+ it("should serialize props", () => {
1340
+ class Model {
1341
+ @Property()
1342
+ id: string;
1343
+
1344
+ @Groups("summary")
1345
+ ignored: boolean;
1346
+
1347
+ @Name("renamed")
1348
+ name: string;
1349
+ }
1350
+
1351
+ const test = new Model();
1352
+ test.id = "id";
1353
+ test.ignored = true;
1354
+ test.name = "myname";
1355
+
1356
+ const result = serialize(test, {type: Model});
1357
+
1358
+ expect(result).toEqual({
1359
+ id: "id",
1360
+ ignored: true,
1361
+ renamed: "myname"
1362
+ });
1363
+ });
1364
+ });
1365
+ describe("when jsonMapper.strictGroups = true", () => {
1366
+ beforeEach(() => {
1367
+ JsonMapperSettings.strictGroups = true;
1368
+ });
1369
+ afterEach(() => {
1370
+ JsonMapperSettings.strictGroups = false;
1371
+ });
1372
+ it("should serialize props", () => {
1373
+ class Model {
1374
+ @Property()
1375
+ id: string;
1376
+
1377
+ @Groups("summary")
1378
+ ignored: boolean;
1379
+
1380
+ @Name("renamed")
1381
+ name: string;
1382
+ }
1383
+
1384
+ const test = new Model();
1385
+ test.id = "id";
1386
+ test.ignored = true;
1387
+ test.name = "myname";
1388
+
1389
+ const result = serialize(test, {type: Model});
1390
+
1391
+ expect(result).toEqual({
1392
+ id: "id",
1393
+ renamed: "myname"
1394
+ });
1395
+ });
1396
+ });
1397
+ });
1398
+ describe("custom date mapper", () => {
1399
+ it("should use a custom date mapper", () => {
1400
+ class Test {
1401
+ @Property() myDate: Date;
1402
+ }
1403
+
1404
+ class CustomDateMapper {
1405
+ deserialize(data: string | number): Date;
1406
+ deserialize(data: boolean | null | undefined): boolean | null | undefined;
1407
+ deserialize(data: any): any {
1408
+ // don't convert unexpected data. In normal case, Ajv reject unexpected data.
1409
+ // But by default, we have to skip data deserialization and let user to apply
1410
+ // the right mapping
1411
+ if (isBoolean(data) || data === null || data === undefined) {
1412
+ return data;
1413
+ }
1414
+
1415
+ return new Date(data);
1416
+ }
1417
+
1418
+ serialize(object: Date): any {
1419
+ return new Date(object).toDateString();
1420
+ }
1421
+ }
1422
+
1423
+ const obj = new Test();
1424
+ obj.myDate = new Date("2022-10-02");
1425
+
1426
+ const types = new Map<any, any>(getJsonMapperTypes());
1427
+ types.set(Date, new CustomDateMapper());
1428
+
1429
+ const result = serialize(obj, {
1430
+ types
1431
+ });
1432
+ expect(result).toEqual({
1433
+ myDate: "Sun Oct 02 2022"
1434
+ });
1435
+ });
1436
+ });
1437
+ describe("discriminator", () => {
1438
+ it("should serialize items", () => {
1439
+ class Event {
1440
+ @DiscriminatorKey() // declare this property as discriminator key
1441
+ type: string;
1442
+
1443
+ @Property()
1444
+ value: string;
1445
+ }
1446
+
1447
+ class SubEvent extends Event {
1448
+ @Property()
1449
+ metaSub: string;
1450
+ }
1451
+
1452
+ @DiscriminatorValue("page_view") // or @DiscriminatorValue() value can be inferred by the class name
1453
+ class PageView extends SubEvent {
1454
+ @Required()
1455
+ url: string;
1456
+ }
1457
+
1458
+ @DiscriminatorValue("action", "click_action")
1459
+ class Action extends SubEvent {
1460
+ @Required()
1461
+ event: string;
1462
+ }
1463
+
1464
+ @DiscriminatorValue()
1465
+ class CustomAction extends Event {
1466
+ @Required()
1467
+ event: string;
1468
+
1469
+ @Property()
1470
+ meta: string;
1471
+ }
1472
+
1473
+ type OneOfEvents = PageView | Action | CustomAction;
1474
+
1475
+ const event = new Event();
1476
+ event.value = "value";
1477
+
1478
+ const pageView = new PageView();
1479
+ pageView.value = "value";
1480
+ pageView.url = "url";
1481
+
1482
+ const action = new Action();
1483
+ action.value = "value";
1484
+ action.event = "event";
1485
+
1486
+ const list = [event, pageView, action];
1487
+
1488
+ expect(serialize(list)).toEqual([
1489
+ {
1490
+ value: "value"
1491
+ },
1492
+ {
1493
+ type: "page_view",
1494
+ url: "url",
1495
+ value: "value"
1496
+ },
1497
+ {
1498
+ type: "action",
1499
+ event: "event",
1500
+ value: "value"
1501
+ }
1502
+ ]);
1503
+ });
1504
+ });
1505
+ });