cloudevent.js

const Spec1 = require("./bindings/http/v1/spec_1.js");
const Spec03 = require("./bindings/http/v03/spec_0_3.js");
const Formatter = require("./formats/json/formatter.js");

const { SPEC_V1, SPEC_V03 } = require("./bindings/http/constants.js");
const { isBinary } = require("./bindings/http/validation/fun.js");

/**
 * An CloudEvent describes event data in common formats to provide
 * interopability across services, platforms and systems.
 * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
 */
class CloudEvent {
  /**
   * Creates a new CloudEvent instance
   * @param {object} options CloudEvent properties as a simple object
   * @param {string} options.source Identifies the context in which an event happened as a URI reference
   * @param {string} options.type Describes the type of event related to the originating occurrence
   * @param {string} [options.id] A unique ID for this event - if not supplied, will be autogenerated
   * @param {string} [options.time] A timestamp for this event. May also be provided as a Date
   * @param {string} [options.subject] Describes the subject of the event in the context of the event producer
   * @param {string} [options.dataContentType] The mime content type for the event data
   * @param {string} [options.dataSchema] The URI of the schema that the event data adheres to (v1.0 events)
   * @param {string} [options.schemaURL]  The URI of the schema that the event data adheres to (v0.3 events)
   * @param {string} [options.dataContentEncoding] The content encoding for the event data (v0.3 events)
   * @param {string} [options.specversion] The CloudEvent specification version for this event - default: 1.0
   * @param {*} [options.data] The event payload
   */
  constructor({
    id,
    source,
    type,
    dataContentType,
    time,
    subject,
    dataSchema,
    schemaURL,
    dataContentEncoding,
    data,
    specversion = SPEC_V1 } = {
      id: undefined,
      source: undefined,
      type: undefined,
      dataContentType: undefined,
      time: undefined,
      subject: undefined,
      dataSchema: undefined,
      schemaURL: undefined,
      dataContentEncoding: undefined,
      data: undefined
    }) {

    if (!type || !source) {
      throw new TypeError("event type and source are required");
    }

    switch (specversion) {
      case SPEC_V1:
        this.spec = new Spec1();
        break;
      case SPEC_V03:
        this.spec = new Spec03();
        break;
      default:
        throw new TypeError(`unknown specification version ${specversion}`);
    }
    this.source = source;
    this.type = type;
    this.dataContentType = dataContentType;
    this.data = data;
    this.subject = subject;

    if (dataSchema) {
      this.dataSchema = dataSchema;
    }

    // TODO: Deprecated in 1.0
    if (dataContentEncoding) {
      this.dataContentEncoding = dataContentEncoding;
    }

    // TODO: Deprecated in 1.0
    if (schemaURL) {
      this.schemaURL = schemaURL;
    }

    if (id) {
      this.id = id;
    }

    if (time) {
      this.time = time;
    }
    this.formatter = new Formatter();
  }

  /**
   * Gets or sets the event id. Source + id must be unique for each distinct event.
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#id
   * @type {string}
  */
  get id() {
    return this.spec.id;
  }

  set id(id) {
    this.spec.id = id;
  }

  /**
   * Gets or sets the origination source of this event as a URI.
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#source-1
   */
  get source() {
    return this.spec.source;
  }

  set source(source) {
    this.spec.source = source;
  }

  /**
   * Gets the CloudEvent specification version
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#specversion
   */
  get specversion() {
    return this.spec.specversion;
  }

  /**
   * Gets or sets the event type
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#type
   */
  get type() {
    return this.spec.type;
  }

  set type(type) {
    this.spec.type = type;
  }

  /**
   * Gets or sets the content type of the data value for this event
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype
   */
  get dataContentType() {
    return this.spec.dataContentType;
  }

  set dataContentType(contenttype) {
    this.spec.dataContentType = contenttype;
  }

  /**
   * Gets or sets the event's data schema
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#dataschema
   */
  get dataSchema() {
    if (this.spec instanceof Spec1) {
      return this.spec.dataSchema;
    }
    throw new TypeError("cannot get dataSchema from version 0.3 event");
  }

  set dataSchema(dataschema) {
    if (this.spec instanceof Spec1) {
      this.spec.dataSchema = dataschema;
    } else {
      throw new TypeError("cannot set dataSchema on version 0.3 event");
    }
  }

  /**
   * Gets or sets the event's data content encoding
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/v0.3/spec.md#datacontentencoding
   */
  get dataContentEncoding() {
    if (this.spec instanceof Spec03) {
      return this.spec.dataContentEncoding;
    }
    throw new TypeError("cannot get dataContentEncoding from version 1.0 event");
  }

  set dataContentEncoding(dataContentEncoding) {
    if (this.spec instanceof Spec03) {
      this.spec.dataContentEncoding = dataContentEncoding;
    } else {
      throw new TypeError("cannot set dataContentEncoding on version 1.0 event");
    }
  }

  /**
   * Gets or sets the event subject
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#subject
   */
  get subject() {
    return this.spec.subject;
  }

  set subject(subject) {
    this.spec.subject = subject;
  }

  /**
   * Gets or sets the timestamp for this event as an ISO formatted date string
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#time
   */
  get time() {
    return this.spec.time;
  }

  set time(time) {
    this.spec.time = new Date(time).toISOString();
  }

  /**
   * DEPRECATED: Gets or sets the schema URL for this event. Throws {TypeError}
   * if this is a version 1.0 event.
   * @type {string}
   * @see https://github.com/cloudevents/spec/blob/v0.3/spec.md#schemaurl
   */
  get schemaURL() {
    if (this.spec instanceof Spec03) {
      return this.spec.schemaURL;
    }
    throw new TypeError("cannot get schemaURL from version 1.0 event");
  }

  // TODO: Deprecated in 1.0
  set schemaURL(schemaurl) {
    if (schemaurl && (this.spec instanceof Spec03)) {
      this.spec.schemaURL = schemaurl;
    } else if (schemaurl) {
      throw new TypeError("cannot set schemaURL on version 1.0 event");
    }
  }


  /**
   * Gets or sets the data for this event
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#event-data
   * @type {*}
   */
  get data() {
    return this.spec.data;
  }

  set data(data) {
    this.spec.data = data;
  }

  /**
   * Formats the CloudEvent as JSON. Validates the event according
   * to the CloudEvent specification and throws an exception if
   * it's invalid.
   * @returns {JSON} the CloudEvent in JSON form
   * @throws {ValidationError} if this event cannot be validated against the specification
   */
  format() {
    this.spec.check();
    const payload = {
      data: undefined,
      data_base64: undefined,
      ...this.spec.payload
    };

    // Handle when is binary, creating the data_base64
    if (isBinary(payload.data)) {
      // TODO: The call to this.spec.data formats the binary data
      // I think having a side effect like this is an anti-pattern.
      // FIXIT
      payload.data_base64 = this.spec.data;
      delete payload.data;
    } else {
      delete payload.data_base64;
    }
    return this.formatter.format(payload);
  }

  /**
   * Formats the CloudEvent as JSON. No specification validation is performed.
   * @returns {string} the CloudEvent as a JSON string
   */
  toString() {
    return this.formatter.toString(this.spec.payload);
  }

  /**
   * Adds an extension attribute to this CloudEvent
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes
   * @param {string} key the name of the extension attribute
   * @param {*} value the value of the extension attribute
   * @returns {void}
   */
  addExtension(key, value) {
    this.spec.addExtension(key, value);
    this.extensions = { [key]: value, ...this.extensions };
  }

  /**
   * Gets the extension attributes, if any, associated with this event
   * @see https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes
   * @returns {Object} the extensions attributes - if none exist will will be {}
   */
  getExtensions() {
    return this.extensions;
  }
}

module.exports = CloudEvent;