/
cloudevent.js
1010 lines (959 loc) · 40.9 KB
/
cloudevent.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2018-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
/**
* CloudEvent:
* this module exports some useful definition and utility related to CloudEvents.
*/
/**
* Reference to cloudevent Validator class.
* @private
* @see Validator
*/
const V = require('./validator') // get validator from here
/**
* Reference to cloudevent Transformer class.
* @private
* @see Transformer
*/
const T = require('./transformer') // get transformer from here
/**
* CloudEvent implementation.
*
* @see https://github.com/cloudevents/spec/blob/master/json-format.md
*/
class CloudEvent {
/**
* Create a new instance of a CloudEvent object.
* @param {!string} id the ID of the event (unique), mandatory
* @param {!string} type the type of the event (usually prefixed with a reverse-DNS name), mandatory
* @param {!uri} source the source uri of the event (use '/' if empty), mandatory
* @param {?(object|Map|Set|string)} data the real event data
* @param {object} [options={}] optional attributes of the event; some has default values chosen here:
* - time (timestamp in string ISO representation, from a date, default now),
* - datainbase64 (string) base64 encoded value for the data (data attribute must not be present when this is defined),
* - datacontenttype (string, default 'application/json') is the content type of the data attribute,
* - dataschema (uri) optional, reference to the schema that data adheres to,
* - subject (string) optional, describes the subject of the event in the context of the event producer (identified by source),
* - strict (boolean, default false) tell if object instance will be validated in a more strict way
* @param {?object} extensions optional, contains extension properties (each extension as a key/value property, and no nested objects) but if given any object must contain at least 1 property
* @throws {Error} if strict is true and id or type is undefined or null
* @throws {Error} if data and data_base64 are defined
*/
constructor (id, type, source, data, {
time = new Date(),
datainbase64,
datacontenttype = CloudEvent.datacontenttypeDefault(),
dataschema,
subject,
strict = false
} = {},
extensions
) {
if (strict === true) {
if (!id || !type || !source) {
throw new Error('Unable to create CloudEvent instance, mandatory attributes missing')
}
if (V.isDefinedAndNotNull(data) && V.isDefinedAndNotNull(datainbase64)) {
throw new Error('Unable to create CloudEvent instance, data and data_base64 attributes are exclusive')
}
}
/**
* The event ID.
* @type {string}
* @private
*/
this.id = id
/**
* The event type.
* @type {string}
* @private
*/
this.type = type
/**
* The source URI of the event.
* @type {uri}
* @private
*/
this.source = source
/**
* The real event data.
* Usually it's an object, but could be even a Map or a Set, or a string.
* Copy the original object to avoid changing objects that could be shared.
* @type {(object|Map|Set)}
* @private
*/
if (V.isString(data)) {
// handle an edge case: if the given data is a String, I need to clone in a different way ...
this.data = data.slice()
} else if (V.isObjectOrCollectionOrString(data)) {
// normal case
this.data = { ...data }
} else {
// anything other, assign as is (and let validator complain later if needed)
this.data = data
}
/**
* The CloudEvent specification version.
* @type {string}
* @private
*/
this.specversion = this.constructor.version()
/**
* The real event data, but encoded in base64 format.
* @type {string}
* @private
*/
this.data_base64 = datainbase64
/**
* The MIME Type for the encoding of the data attribute, when serialized.
* If null, default value will be set.
* @type {string}
* @private
*/
this.datacontenttype = (!V.isNull(datacontenttype)) ? datacontenttype : CloudEvent.datacontenttypeDefault()
/**
* The URI of the schema for event data, if any.
* @type {uri}
* @private
*/
this.dataschema = dataschema
/**
* The event timestamp.
* If null, current timestamp will be set
* but directly transformed into a string representation.
* If a not empty string is passed here, it will be set
* (then later validator could check it).
* See under for more details.
* @type {object}
* @private
*/
this.time = (V.isNull(time)) ? T.timestampToString(new Date()) : null
/**
* The subject of the event in the context of the event producer.
* @type {string}
* @private
*/
this.subject = subject
// set time depending on the given input type, for be safer
if (V.isDate(time)) {
// convert the given timestamp in the right string representation
this.time = T.timestampToString(time)
} else if (V.isStringNotEmpty(time)) {
// assign the value directly, maybe the validator will check later
this.time = time
}
// add strict to extensions, but only when defined
if (strict === true) {
this.constructor.setStrictExtensionInEvent(this, strict)
const extensionsSize = V.getSize(extensions)
if (extensionsSize < 1) {
throw new Error('Unable to create CloudEvent instance, extensions must contain at least 1 property')
}
if (V.doesObjectContainsStandardProperty(extensions, CloudEvent.isStandardProperty)) {
throw new Error('Unable to create CloudEvent instance, extensions contains standard properties')
}
}
// set extensions
this.constructor.setExtensionsInEvent(this, extensions)
}
/**
* Return the version of the CloudEvent Specification implemented here
*
* @static
* @return {string} the value
*/
static version () {
return '1.0'
}
/**
* Return the default data content Type for a CloudEvent
*
* @static
* @return {string} the value
*/
static datacontenttypeDefault () {
return 'application/json'
}
/**
* Return the MIME Type for a CloudEvent
*
* @static
* @return {string} the value
*/
static mediaType () {
return 'application/cloudevents+json'
}
/**
* Tell the data content Type for a CloudEvent,
* if is a JSON-derived format,
* so data must be encoded/decoded accordingly.
*
* @static
* @param {!object} event the CloudEvent to validate
* @return {boolean} true if data content type is JSON-like, otherwise false
* @throws {TypeError} if event is not a CloudEvent instance or subclass
* @throws {Error} if event is undefined or null
*/
static isDatacontenttypeJSONEvent (event) {
if (!CloudEvent.isCloudEvent(event)) {
throw new TypeError('The given event is not a CloudEvent instance')
}
return (
(event.datacontenttype === CloudEvent.datacontenttypeDefault()) ||
(event.datacontenttype.endsWith('/json')) ||
(event.datacontenttype.endsWith('+json'))
)
}
/**
* Tell if the object has the strict flag enabled.
*
* @static
* @param {!object} event the CloudEvent to validate
* @return {boolean} true if strict, otherwise false
* @throws {TypeError} if event is not a CloudEvent instance or subclass
* @throws {Error} if event is undefined or null
*/
static isStrictEvent (event) {
if (!CloudEvent.isCloudEvent(event)) {
throw new TypeError('The given event is not a CloudEvent instance')
}
if (V.isDefinedAndNotNull(event.strictvalidation)) {
return event.strictvalidation === true
} else {
return false
}
}
/**
* Set the strict flag into the given extensions object.
* Should not be used outside CloudEvent constructor.
*
* @private
* @static
* @param {object} [obj={}] the object with extensions to fill (maybe already populated), that will be enhanced inplace
* @param {boolean} [strict=false] the flag to set (default false)
* @throws {TypeError} if obj is not an object, or strict is not a flag
* @throws {Error} if obj is undefined or null, or strict is undefined or null
*/
static setStrictExtensionInEvent (obj = {}, strict = false) {
if (!V.isObject(obj)) {
throw new TypeError('The given extensions is not an object instance')
}
if (!V.isBoolean(strict)) {
throw new TypeError('The given strict flag is not a boolean instance')
}
obj.strictvalidation = strict
}
/**
* Get the strict flag from the given extensions object.
* Should not be used outside CloudEvent.
*
* @private
* @static
* @param {object} [obj={}] the object with extensions to check
* @return {boolean} the strict flag value, or false if not found
* @throws {TypeError} if obj is not an object, or strict is not a flag
* @throws {Error} if obj is undefined or null
* @throws {Error} if strictvalidation property is undefined or null
*/
static getStrictExtensionOfEvent (obj = {}) {
if (!V.isObject(obj)) {
throw new TypeError('The given extensions is not an object instance')
}
const myExtensionStrict = obj.strictvalidation || false
if (!V.isBoolean(myExtensionStrict)) {
throw new TypeError("Extension property 'strictvalidation' has not a boolean value")
}
return myExtensionStrict
}
/**
* Set all extensions into the given object.
* Should not be used outside CloudEvent constructor.
*
* @private
* @static
* @param {object} [obj={}] the object to fill, that will be enhanced inplace
* @param {object} [extensions=null] the extensions to fill (each extension as a key/value property, and no nested properties)
* @throws {TypeError} if obj is not an object, or strict is not a flag
* @throws {Error} if obj is undefined or null, or strict is undefined or null
*/
static setExtensionsInEvent (obj = {}, extensions = null) {
if (!V.isObject(obj)) {
throw new TypeError('The given obj is not an object instance')
}
if (!V.isDefinedAndNotNull(extensions)) {
return
}
if (V.isObject(extensions)) {
const exts = Object.entries(extensions).filter(i => !V.doesStringIsStandardProperty(i[0], CloudEvent.isStandardProperty))
// add filtered extensions to the given obj
for (const [key, value] of exts) {
obj[key] = value
}
} else {
throw new TypeError('Unsupported extensions: not an object or a string')
}
}
/**
* Get all extensions (non standard) properties from the given object.
* Should not be used outside CloudEvent.
*
* @private
* @static
* @param {object} [obj={}] the object to check
* @return {object} an object containins all extensions (non standard properties) found
* @throws {TypeError} if obj is not an object
* @throws {Error} if obj is undefined or null
*/
static getExtensionsOfEvent (obj = {}) {
const extensions = {}
if (V.isObject(obj)) {
const exts = Object.entries(obj).filter(i => !V.doesStringIsStandardProperty(i[0], CloudEvent.isStandardProperty))
if (exts.length > 0) {
// add filtered extensions to the given extensions
for (const [key, value] of exts) {
extensions[key] = value
}
} else {
// no extensions found, so return a null value
// (extensions defined but empty are not valid extensions)
return null
}
} else {
throw new TypeError('Unsupported extensions: not an object or a string')
}
return extensions
}
/**
* Validate the given CloudEvent.
*
* @static
* @param {!object} event the CloudEvent to validate
* @param {object} [options={}] containing:
* - strict (boolean, default null so no override) to validate it in a more strict way (if null it will be used strict mode in the given event),
* - dataschemavalidator (function(data, dataschema) boolean, optional) a function to validate data of current CloudEvent instance with its dataschema
* - timezoneOffset (number, default 0) to apply a different timezone offset
* @return {object[]} an array of (non null) validation errors, or at least an empty array
*/
static validateEvent (event, {
strict = null,
dataschemavalidator = null,
timezoneOffset = 0
} = {}) {
if (V.isUndefinedOrNull(event)) {
return [new Error('CloudEvent undefined or null')]
}
if (!CloudEvent.isCloudEvent(event)) {
return [new TypeError(`The argument must be a CloudEvent (or a subclass), instead got a '${typeof event}'`)]
}
const ve = [] // validation errors
// standard validation
// note that some properties are not checked here because I assign a default value, and I check them in strict mode, like:
// data, time, extensions, datacontenttype ...
// ve.push(V.ensureIsStringNotEmpty(event.specversion, 'specversion')) // no more a public attribute
ve.push(V.ensureIsStringNotEmpty(event.id, 'id'))
ve.push(V.ensureIsStringNotEmpty(event.type, 'type'))
ve.push(V.ensureIsStringNotEmpty(event.source, 'source'))
if (V.isDefinedAndNotNull(event.dataschema)) {
ve.push(V.ensureIsStringNotEmpty(event.dataschema, 'dataschema'))
}
if (V.isDefinedAndNotNull(event.subject)) {
ve.push(V.ensureIsStringNotEmpty(event.subject, 'subject'))
}
if (V.isDefinedAndNotNull(event.data_base64)) {
ve.push(V.ensureIsStringNotEmpty(event.data_base64, 'data_base64'))
if (V.isDefinedAndNotNull(event.data)) {
ve.push(new Error('data and data_base64 attributes are exclusive'))
}
}
// additional validation if strict mode enabled, or if enabled in the event ...
if (strict === true || (strict === null && CloudEvent.isStrictEvent(event) === true)) {
ve.push(V.ensureIsVersion(event.specversion, 'specversion'))
if (V.isDefinedAndNotNull(event.data)) {
if (event.datacontenttype === CloudEvent.datacontenttypeDefault()) {
// if it's a string, ensure it's a valid JSON representation,
// otherwise ensure data is a plain object or collection, but not a string in this case
if (V.isString(event.data)) {
try {
JSON.parse(event.data)
} catch (e) {
ve.push(new Error('data is not a valid JSON string'))
}
} else {
ve.push(CloudEvent.ensureTypeOfDataIsRight(event))
}
// end of default datacontenttype
} else {
// ensure data is a plain object or collection,
// or even a value (string or boolean or number) in this case
// because in serialization/deserialization some validation can occur on the transformed object
ve.push(CloudEvent.ensureTypeOfDataIsRight(event))
}
}
ve.push(V.ensureIsURI(event.source, null, 'source'))
ve.push(V.ensureIsDatePast(T.timestampFromString(event.time, timezoneOffset), 'time'))
ve.push(V.ensureIsStringNotEmpty(event.datacontenttype, 'datacontenttype'))
if (V.isDefinedAndNotNull(event.dataschema)) {
ve.push(V.ensureIsURI(event.dataschema, null, 'dataschema'))
}
if (V.isFunction(dataschemavalidator)) {
try {
const success = dataschemavalidator(event.data, event.dataschema)
if (success === false) throw Error()
} catch (e) {
ve.push(new Error(`data does not respect the dataschema '${event.dataschema}' for the given validator`))
}
}
if (V.isDefinedAndNotNull(event.extensions)) {
// get extensions via its getter
ve.push(V.ensureIsObjectOrCollectionNotString(event.extensions, 'extensions'))
// error for extensions defined but empty (without properties), moved in constructor
// then check for each extension name and value
for (const [key, value] of Object.entries(event.extensions)) {
if (!CloudEvent.isExtensionNameValid(key)) ve.push(new Error(`extension name '${key}' not valid`))
if (!CloudEvent.isExtensionValueValid(value)) ve.push(new Error(`extension value '${value}' not valid for extension '${key}'`))
}
}
}
return ve.filter((i) => i)
}
/**
* Tell the given CloudEvent, if it's valid.
*
* See {@link CloudEvent.validateEvent}.
*
* @static
* @param {!object} event the CloudEvent to validate
* @param {object} [options={}] containing:
* - strict (boolean, default null so no override) to validate it in a more strict way (if null it will be used strict mode in the given event),
* - dataschemavalidator (function(data, dataschema) boolean, optional) a function to validate data of current CloudEvent instance with its dataschema
* - printDebugInfo (boolean, default false) to print some debug info to the console,
* - timezoneOffset (number, default 0) to apply a different timezone offset
* @return {boolean} true if valid, otherwise false
*/
static isValidEvent (event, {
strict = null,
dataschemavalidator = null,
printDebugInfo = false,
timezoneOffset = 0
} = {}) {
const validationErrors = CloudEvent.validateEvent(event, { strict, dataschemavalidator, timezoneOffset })
const size = V.getSize(validationErrors)
if (printDebugInfo === true) { // print some debug info
console.log(`DEBUG | validation errors found: ${size}, details: ${JSON.stringify(validationErrors)}`)
}
return (size === 0)
}
/**
* Tell the given CloudEvent, if it's instance of the CloudEvent class or a subclass of it.
*
* @static
* @param {!object} event the CloudEvent to check
* @return {boolean} true if it's an instance (or a subclass), otherwise false
* @throws {Error} if event is undefined or null
*/
static isCloudEvent (event) {
if (V.isUndefinedOrNull(event)) {
throw new Error('CloudEvent undefined or null')
}
return V.isClass(event, CloudEvent)
}
/**
* Serialize the given CloudEvent in JSON format.
* Note that here standard serialization to JSON is used (no additional libraries).
* Note that the result of encoder function is assigned to encoded data.
*
* @static
* @param {!object} event the CloudEvent to serialize
* @param {object} [options={}] optional serialization attributes:
* - encoder (function, no default) a function that takes data and returns encoded data as a string,
* - encodedData (string, no default) already encoded data (but consistency with the datacontenttype is not checked),
* - onlyValid (boolean, default false) to serialize only if it's a valid instance,
* - onlyIfLessThan64KB (boolean, default false) to return the serialized string only if it's less than 64 KB,
* - printDebugInfo (boolean, default false) to print some debug info to the console,
* - timezoneOffset (number, default 0) to apply a different timezone offset
* @return {string} the serialized event, as a string
* @throws {Error} if event is undefined or null, or an option is undefined/null/wrong
*/
static serializeEvent (event, {
encoder, encodedData,
onlyValid = false, onlyIfLessThan64KB = false,
printDebugInfo = false,
timezoneOffset = 0
} = {}) {
if (V.isUndefinedOrNull(event)) throw new Error('CloudEvent undefined or null')
if (printDebugInfo === true) {
console.log(`DEBUG | trying to serialize ce: ${JSON.stringify(event)}`)
}
if (event.datacontenttype === CloudEvent.datacontenttypeDefault()) {
if ((onlyValid === false) || (onlyValid === true && CloudEvent.isValidEvent(event, { timezoneOffset }) === true)) {
const ser = JSON.stringify(event, function replacer (key, value) {
switch (key) {
case 'extensions':
// filtering out top level extensions (if any)
return undefined
default:
return value
}
})
if (printDebugInfo === true) {
console.log(`DEBUG | ce successfully serialized as: ${ser}`)
}
if ((onlyIfLessThan64KB === false) || (onlyIfLessThan64KB === true && V.getSizeInBytes(ser) < 65536)) return ser
else throw new Error('Unable to return a serialized CloudEvent bigger than 64 KB.')
} else throw new Error('Unable to serialize a not valid CloudEvent.')
}
// else (non defaut datacontenttype)
if (V.isDefinedAndNotNull(encoder)) {
if (!V.isFunction(encoder)) throw new Error(`Missing or wrong encoder function: '${encoder}' for the given content type: '${event.datacontenttype}'.`)
encodedData = encoder(event.payload)
} else {
// encoder not defined, check encodedData
// but mandatory only for non-value data
if (!V.isValue(event.data) && !V.isDefinedAndNotNull(encodedData)) throw new Error(`Missing encoder function: use encoder function or already encoded data with the given data content type: '${event.datacontenttype}'.`)
if (V.isValue(event.data) && !V.isDefinedAndNotNull(encodedData)) {
encodedData = `${event.data}`
}
}
if (!V.isStringNotEmpty(encodedData)) throw new Error(`Missing or wrong encoded data: '${encodedData}' for the given data content type: '${event.datacontenttype}'.`)
const newEvent = T.mergeObjects(event, { data: encodedData })
if ((onlyValid === false) || (onlyValid === true && CloudEvent.isValidEvent(newEvent, { timezoneOffset }) === true)) {
const ser = JSON.stringify(newEvent)
if (printDebugInfo === true) {
console.log(`DEBUG | ce successfully serialized as: ${ser}`)
}
if ((onlyIfLessThan64KB === false) || (onlyIfLessThan64KB === true && V.getSizeInBytes(ser) < 65536)) return ser
else throw new Error('Unable to return a serialized CloudEvent bigger than 64 KB.')
} else throw new Error('Unable to serialize a not valid CloudEvent.')
}
/**
* Deserialize/parse the given CloudEvent from JSON format.
* Note that here standard parse from JSON is used (no additional libraries).
* Note that the result of decoder function is assigned to decoded data.
*
* @static
* @param {!string} ser the serialized CloudEvent to parse/deserialize
* @param {object} [options={}] optional deserialization attributes:
* - decoder (function, no default) a function that takes data and returns decoder data as a string,
* - decodedData (string, no default) already decoded data (but consistency with the datacontenttype is not checked),
* - onlyValid (boolean, default false) to deserialize only if it's a valid instance,
* - onlyIfLessThan64KB (boolean, default false) to return the deserialized string only if it's less than 64 KB,
* - printDebugInfo (boolean, default false) to print some debug info to the console,
* - timezoneOffset (number, default 0) to apply a different timezone offset
* @return {object} the deserialized event as a CloudEvent instance
* @throws {Error} if ser is undefined or null, or an option is undefined/null/wrong
* @throws {Error} in case of JSON parsing error
*/
static deserializeEvent (ser, {
decoder, decodedData,
onlyValid = false, onlyIfLessThan64KB = false,
printDebugInfo = false,
timezoneOffset = 0
} = {}) {
if (V.isUndefinedOrNull(ser)) throw new Error('Serialized CloudEvent undefined or null')
if (!V.isStringNotEmpty(ser)) throw new Error(`Missing or wrong serialized data: '${ser}' must be a string and not a: '${typeof ser}'.`)
if (printDebugInfo === true) {
console.log(`DEBUG | trying to deserialize as ce: ${ser}`)
}
// deserialize standard attributes, always in JSON format
const parsed = JSON.parse(ser)
// ensure it's an object (single), and not a string neither a collection or an array
if (!V.isObject(parsed) || V.isArray(parsed)) throw new Error(`Wrong deserialized data: '${ser}' must represent an object and not an array or a string or other.`)
if (!V.isStringNotEmpty(parsed.specversion) || parsed.specversion !== CloudEvent.version()) throw new Error(`Unable to deserialize, not compatible specversion: got '${parsed.specversion}' expected '${CloudEvent.version()}'.`)
const strict = CloudEvent.getStrictExtensionOfEvent(parsed)
const extensions = CloudEvent.getExtensionsOfEvent(parsed)
// fill a new CludEvent instance with parsed data
const ce = new CloudEvent(parsed.id,
parsed.type,
parsed.source,
parsed.data,
{ // options
time: parsed.time,
datainbase64: parsed.data_base64,
datacontenttype: parsed.datacontenttype,
dataschema: parsed.dataschema,
subject: parsed.subject,
strict
},
extensions
)
// depending on the datacontenttype, decode the data attribute (the payload)
if (parsed.datacontenttype === CloudEvent.datacontenttypeDefault()) {
// return ce, depending on its validation option
if (printDebugInfo === true) {
console.log(`DEBUG | ce successfully deserialized as: ${JSON.stringify(ce)}`)
}
if ((onlyValid === false) || (onlyValid === true && CloudEvent.isValidEvent(ce, { timezoneOffset }) === true)) {
if ((onlyIfLessThan64KB === false) || (onlyIfLessThan64KB === true && V.getSizeInBytes(ser) < 65536)) return ce
else throw new Error('Unable to return a deserialized CloudEvent bigger than 64 KB.')
} else throw new Error('Unable to deserialize a not valid CloudEvent.')
}
// else (non defaut datacontenttype)
if (V.isDefinedAndNotNull(decoder)) {
if (!V.isFunction(decoder)) throw new Error(`Missing or wrong decoder function: '${decoder}' for the given data content type: '${parsed.datacontenttype}'.`)
decodedData = decoder(parsed.data)
} else {
// decoder not defined, so decodedData must be defined
// but mandatory only for non-value data
if (!V.isValue(parsed.data) && !V.isDefinedAndNotNull(decodedData)) throw new Error(`Missing decoder function: use decoder function or already decoded data with the given data content type: '${parsed.datacontenttype}'.`)
if (V.isValue(parsed.data) && !V.isDefinedAndNotNull(decodedData)) {
decodedData = `${parsed.data}`
}
}
if (!V.isObjectOrCollectionOrArrayOrValue(decodedData)) throw new Error(`Missing or wrong decoded data: '${decodedData}' for the given data content type: '${parsed.datacontenttype}'.`)
// overwrite data with decodedData before returning it
ce.data = decodedData
// return ce, depending on its validation option
if ((onlyValid === false) || (onlyValid === true && CloudEvent.isValidEvent(ce, { timezoneOffset }) === true)) {
if (printDebugInfo === true) {
console.log(`DEBUG | ce successfully deserialized as: ${JSON.stringify(ce)}`)
}
if ((onlyIfLessThan64KB === false) || (onlyIfLessThan64KB === true && V.getSizeInBytes(ser) < 65536)) return ce
else throw new Error('Unable to return a deserialized CloudEvent bigger than 64 KB.')
} else throw new Error('Unable to deserialize a not valid CloudEvent.')
}
/**
* Tell the given property, if it's a standard CloudEvent property/attribute.
*
* @static
* @param {!string} property the property/attribute to check
* @return {boolean} true if it's standard otherwise false
*/
static isStandardProperty (property) {
return CloudEvent.standardProps.includes(property)
}
/**
* Tell the given property, if it's an extension CloudEvent property/attribute.
*
* @static
* @param {!string} property the property/attribute to check
* @return {boolean} true if it's an extension (not standard) otherwise false
*/
static isExtensionProperty (property) {
return !CloudEvent.standardProps.includes(property)
}
/**
* Tell if the given extension name is valid, to respect the spec.
* Should not be used outside CloudEvent.
*
* @private
* @static
* @param {!object|!string} name the name to check
* @return {boolean} true if it's an extension name valid, otherwise false
* @throws {TypeError} if name is not a string
* @throws {Error} if name is undefined or null
*/
static isExtensionNameValid (name) {
if (V.isUndefinedOrNull(name)) throw new Error('Extension name undefined or null')
if (!V.isString(name)) throw new TypeError('Extension name must be a string')
return name.match(/^[a-z0-9]{1,20}$/)
}
/**
* Tell if the given extension value is valid, to respect the spec.
* Should not be used outside CloudEvent.
*
* @private
* @static
* @param {!string|!boolean|!number} value the object to check
* @return {boolean} true if it's an extension value valid, otherwise false
* @throws {Error} if value is undefined
*/
static isExtensionValueValid (value) {
if (V.isUndefined(value)) throw new Error('Extension value undefined')
if (!V.isString(value) && !V.isBoolean(value) && !V.isNumber(value) && !V.isNull(value)) return false
return true
}
/**
* Get the JSON Schema for a CloudEvent.
* Note that it's not used in standard serialization to JSON,
* but only in some serialization libraries.
* Note that schema definitions for data and extensions are right,
* but I need to keep them commented here and to set the flag
* additionalProperties to true,
* or when used both data and extensions will be empty in JSON output.
*
* See JSON Schema.
*
* @static
* @return {object} the JSON Schema
*/
static getJSONSchema () {
// define a schema for serializing a CloudEvent object to JSON
// note that properties not in the schema will be ignored
// (in json output) by some json serialization libraries, if additionalProperties is false
return {
title: 'CloudEvent Schema with required fields',
type: 'object',
properties: {
specversion: { type: 'string', minLength: 1 },
id: { type: 'string', minLength: 1 },
type: { type: 'string', minLength: 1 },
source: { type: 'string', format: 'uri-reference' },
datacontenttype: { type: ['string', 'null'], minLength: 1 },
data: { type: ['object', 'string', 'number', 'array', 'boolean', 'null'] },
data_base64: { type: ['string', 'null'], contentEncoding: 'base64' },
dataschema: { type: ['string', 'null'], format: 'uri', minLength: 1 },
time: { type: ['string', 'null'], format: 'date-time', minLength: 1 },
subject: { type: ['string', 'null'], minLength: 1 }
},
required: ['specversion', 'id', 'type', 'source'],
additionalProperties: true // to handle data, and maybe other (non-standard) properties (extensions)
}
}
/**
* Tell the type of data of the CloudEvent,
* if it's right (depending even on related datacontenttype),
* from the validator point of view.
*
* @static
* @param {!object} ce the CloudEvent to validate
* @param {object} [options={}] optional validation options
* @param {string} [name='data'] the name to assign in the returned error string (if any), or 'data' as default value
* @return {string|null} error message if the given data type is not right, otherwise null
* @throws {TypeError} if event is not a CloudEvent instance or subclass
* @throws {Error} if event is undefined or null
*/
static ensureTypeOfDataIsRight (ce, options = {}, name = 'data') {
if (!CloudEvent.isCloudEvent(ce)) throw new TypeError('The given event is not a CloudEvent instance')
let ve
if (V.isUndefinedOrNull(ce.data)) {
ve = null // it's impossible to verify its type
} else if (ce.datacontenttype === CloudEvent.datacontenttypeDefault()) {
ve = V.ensureIsObjectOrCollectionOrArrayNotValue(ce.data, name) || null
} else {
// for example with: datacontenttype 'text/plain':
// ensure data is a plain object or collection,
// or even a string or boolean or number in this case
// because in serialization/deserialization some validation can occur on the transformed object
ve = V.ensureIsObjectOrCollectionOrArrayOrValue(ce.data, name) || null
}
return ve
}
/**
* Utility function that return a dump of validation results
* on the given CloudEvent.
*
* @static
* @param {(?object)} ce the CloudEvent object to dump
* @param {object} [options={}] optional validation options
* @param {string} [name='noname'] the name to assign in the returned string, or 'noname' as default value
* @return {string} the dump of the object or a message when obj is undefined/null/not a CloudEvent
*/
static dumpValidationResults (ce, options = {}, name = 'noname') {
if (V.isUndefined(ce)) {
return `${name}: undefined`
} else if (V.isNull(ce)) {
return `${name}: null`
} else if (CloudEvent.isCloudEvent(ce)) {
const opts = options ?? {}
const ve = CloudEvent.validateEvent(ce, opts)
return `${name}, validation with options (${JSON.stringify(options)}): ${JSON.stringify(ve.map((i) => i.message))}`
} else {
return `${name}: 'is not a CloudEvent, no validation possible'`
}
}
/**
* Getter method to return the list of standard property names, as an array of strings.
*
* @type {Array}
* @static
*/
static get standardProps () {
return [
'specversion',
'id', 'type', 'source', 'data',
'time', 'data_base64', 'datacontenttype',
'dataschema', 'subject'
]
}
/**
* Serialize the current CloudEvent.
*
* See {@link CloudEvent.serializeEvent}.
*
* @param {object} [options={}] optional serialization attributes:
* - encoder (function, default null) a function that takes data and returns encoded data,
* - encodedData (string, default null) already encoded data (but consistency with the datacontenttype is not checked),
* @return {string} the serialized event, as a string
*/
serialize ({ encoder, encodedData } = {}) {
return this.constructor.serializeEvent(this, { encoder, encodedData })
}
/**
* Validate the current CloudEvent.
*
* See {@link CloudEvent.validateEvent}.
*
* @param {object} [options={}] containing:
* - strict (boolean, default null so no override) to validate it in a more strict way (if null it will be used strict mode in the given event),
* - dataschemavalidator (function(data, dataschema) boolean, optional) a function to validate data of current CloudEvent instance with its dataschema
* @return {object[]} an array of (non null) validation errors, or at least an empty array
*/
validate ({ strict = null, dataschemavalidator = null } = {}) {
return this.constructor.validateEvent(this, { strict, dataschemavalidator })
}
/**
* Tell the current CloudEvent, if it's valid.
*
* See {@link CloudEvent.isValidEvent}.
*
* @param {object} [options={}] containing:
* - strict (boolean, default null so no override) to validate it in a more strict way (if null it will be used strict mode in the given event),
* - dataschemavalidator (function(data, dataschema) boolean, optional) a function to validate data of current CloudEvent instance with its dataschema
* - printDebugInfo (boolean, default false) to print some debug info to the console,
* - timezoneOffset (number, default 0) to apply a different timezone offset
* @return {boolean} true if valid, otherwise false
*/
isValid ({ strict = null, dataschemavalidator = null, printDebugInfo = false, timezoneOffset = 0 } = {}) {
return this.constructor.isValidEvent(this, { strict, dataschemavalidator, printDebugInfo, timezoneOffset })
}
/**
* Getter method to tell if data content type is a JSON-derived format,
* so data must be encoded/decoded accordingly.
*
* See {@link CloudEvent.isDatacontenttypeJSONEvent}.
*
* @type {boolean}
*/
get isDatacontenttypeJSON () {
return this.constructor.isDatacontenttypeJSONEvent(this)
}
/**
* Getter method to tell if the object has the strict flag enabled.
*
* See {@link CloudEvent.isStrictEvent}.
*
* @type {boolean}
*/
get isStrict () {
return this.constructor.isStrictEvent(this)
}
/**
* Getter method to return JSON Schema for a CloudEvent.
*
* See {@link CloudEvent.getJSONSchema}.
*
* @type {object}
*/
get schema () {
return this.constructor.getJSONSchema()
}
/**
* Getter method to return the CloudEvent time but as a Date object.
*
* See {@link CloudEvent.time}.
*
* @type {Date}
*/
get timeAsDate () {
return T.timestampFromString(this.time)
}
/**
* Getter method to return a copy of CloudEvent data attribute (or data_base64 if defined),
* but transformed/decoded if possible.
*
* See {@link CloudEvent.data}, {@link CloudEvent.data_base64}.
*
* @type {(object|Map|Set|Array|string|boolean|number)}
*/
get payload () {
if (V.isDefinedAndNotNull(this.data) && !V.isDefinedAndNotNull(this.data_base64)) {
if (this.isDatacontenttypeJSON) {
try {
return JSON.parse(this.data)
} catch (e) {
// fallback in case of bad data (not parseable)
if (V.isString(this.data)) {
return this.data.slice()
} else if (V.isArray(this.data)) {
return this.data.map((i) => i)
} else {
return { ...this.data }
}
}
// end of this.isDatacontenttypeJSON
} else if (V.isString(this.data)) {
return this.data.slice()
} else if (V.isArray(this.data)) {
return this.data.map((i) => i)
} else if (V.isBoolean(this.data) || V.isNumber(this.data)) {
return this.data
} else {
return { ...this.data }
}
} else if (V.isDefinedAndNotNull(this.data_base64)) {
return T.stringFromBase64(this.data_base64)
}
// else return the same empty object
return this.data
}
/**
* Getter method to tell if CloudEvent data is text or binary,
* or unknown if not clear.
*
* @type {string}
*/
get dataType () {
if (V.isDefinedAndNotNull(this.data) && !V.isDefinedAndNotNull(this.data_base64)) {
return 'Text'
} else if (V.isDefinedAndNotNull(this.data_base64)) {
return 'Binary'
}
// else return an unknown/wrong data type
return 'Unknown'
}
/**
* Getter method to return a copy of CloudEvent extensions.
*
* See {@link CloudEvent.getExtensionsOfEvent}.
*
* @type {object}
*/
get extensions () {
return this.constructor.getExtensionsOfEvent(this)
}
/**
* Override the usual toString method,
* to show a summary (only some info) on current instance.
* Note that the representation of the 'data' attribute is
* limited to 1024 chars (arbitrary limit, set here including the trim marker),
* to avoid too much overhead with instances with a big 'data' attribute.
*
* See {@link Object.toString}.
*
* @return {string} a string representation for object instance
*/
toString () {
const payload = this.payload
const payloadDump = T.dumpObject(payload, 'payload')
let payloadSummary = (payloadDump.length < 1024) ? payloadDump : (payloadDump.substring(0, 1021) + '...')
if (V.isString(payload)) {
payloadSummary = payloadSummary + '\''
}
return `CloudEvent[specversion:${this.specversion}, id:'${this.id}', type:'${this.type}', source:'${this.source}', datacontenttype:'${this.datacontenttype}', ${payloadSummary}, ...]`
}
/**
* Gives a string valued property that is used in the creation of the default string description of an object.
*