UNPKG

cloudevent

Version:

JavaScript/Node.js implementation of the CloudEvents standard format

1,011 lines (959 loc) 41.9 kB
/* * Copyright 2018-2022 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. * * See {@link Symbol.toStringTag}. * * @return {string} a string representation of the object type */ get [Symbol.toStringTag] () { return 'CloudEvent' } } module.exports = CloudEvent