diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index cef0675f..8c67baa3 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -147,7 +147,7 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`); toJSON(): Record { const event = { ...this }; event.time = new Date(this.time as string).toISOString(); - event.data = !isBinary(this.data) ? this.data : undefined; + event.data = this.#_data; return event; } @@ -184,30 +184,30 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`); } /** - * Clone a CloudEvent with new/update attributes - * @param {object} options attributes to augment the CloudEvent with an `data` property + * Clone a CloudEvent with new/updated attributes + * @param {object} options attributes to augment the CloudEvent without a `data` property * @param {boolean} strict whether or not to use strict validation when cloning (default: true) * @throws if the CloudEvent does not conform to the schema * @return {CloudEvent} returns a new CloudEvent */ public cloneWith(options: Partial, "data">>, strict?: boolean): CloudEvent; /** - * Clone a CloudEvent with new/update attributes - * @param {object} options attributes to augment the CloudEvent with a `data` property + * Clone a CloudEvent with new/updated attributes and new data + * @param {object} options attributes to augment the CloudEvent with a `data` property and type * @param {boolean} strict whether or not to use strict validation when cloning (default: true) * @throws if the CloudEvent does not conform to the schema * @return {CloudEvent} returns a new CloudEvent */ - public cloneWith(options: Partial>, strict?: boolean): CloudEvent; + public cloneWith(options: Partial>, strict?: boolean): CloudEvent; /** - * Clone a CloudEvent with new/update attributes + * Clone a CloudEvent with new/updated attributes and possibly different data types * @param {object} options attributes to augment the CloudEvent * @param {boolean} strict whether or not to use strict validation when cloning (default: true) * @throws if the CloudEvent does not conform to the schema * @return {CloudEvent} returns a new CloudEvent */ public cloneWith(options: Partial>, strict = true): CloudEvent { - return new CloudEvent(Object.assign({}, this.toJSON(), options), strict); + return CloudEvent.cloneWith(this, options, strict); } /** @@ -217,4 +217,22 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`); [Symbol.for("nodejs.util.inspect.custom")](): string { return this.toString(); } + + /** + * Clone a CloudEvent with new or updated attributes. + * @param {CloudEventV1} event an object that implements the {@linkcode CloudEventV1} interface + * @param {Partial>} options an object with new or updated attributes + * @param {boolean} strict `true` if the resulting event should be valid per the CloudEvent specification + * @throws {ValidationError} if `strict` is `true` and the resulting event is invalid + * @returns {CloudEvent} a CloudEvent cloned from `event` with `options` applied. + */ + public static cloneWith( + event: CloudEventV1, + options: Partial>, + strict = true): CloudEvent { + if (event instanceof CloudEvent) { + event = event.toJSON() as CloudEventV1; + } + return new CloudEvent(Object.assign({}, event, options), strict); + } } diff --git a/src/message/http/headers.ts b/src/message/http/headers.ts index 510488ff..e8a9c708 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -4,7 +4,7 @@ */ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; -import { CloudEvent } from "../.."; +import { CloudEventV1 } from "../.."; import { Headers } from "../"; import { Version } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; @@ -24,7 +24,7 @@ export const requiredHeaders = [ * @param {CloudEvent} event a CloudEvent * @returns {Object} the headers that will be sent for the event */ -export function headersFor(event: CloudEvent): Headers { +export function headersFor(event: CloudEventV1): Headers { const headers: Headers = {}; let headerMap: Readonly<{ [key: string]: MappedParser }>; if (event.specversion === Version.V1) { diff --git a/src/message/http/index.ts b/src/message/http/index.ts index 3934416f..dfde84c3 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -4,7 +4,7 @@ */ import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; -import { Message, Headers } from ".."; +import { Message, Headers, Binding } from ".."; import { headersFor, @@ -25,7 +25,7 @@ import { JSONParser, MappedParser, Parser, parserByContentType } from "../../par * @param {CloudEvent} event The event to serialize * @returns {Message} a Message object with headers and body */ -export function binary(event: CloudEvent): Message { +function binary(event: CloudEventV1): Message { const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; const headers: Headers = { ...contentType, ...headersFor(event) }; let body = event.data; @@ -47,10 +47,10 @@ export function binary(event: CloudEvent): Message { * @param {CloudEvent} event the CloudEvent to be serialized * @returns {Message} a Message object with headers and body */ -export function structured(event: CloudEvent): Message { +function structured(event: CloudEventV1): Message { if (event.data_base64) { // The event's data is binary - delete it - event = event.cloneWith({ data: undefined }); + event = (event as CloudEvent).cloneWith({ data: undefined }); } return { headers: { @@ -67,7 +67,7 @@ export function structured(event: CloudEvent): Message { * @param {Message} message an incoming Message object * @returns {boolean} true if this Message is a CloudEvent */ -export function isEvent(message: Message): boolean { +function isEvent(message: Message): boolean { // TODO: this could probably be optimized try { deserialize(message); @@ -84,7 +84,7 @@ export function isEvent(message: Message): boolean { * @param {Message} message the incoming message * @return {CloudEvent} A new {CloudEvent} instance */ -export function deserialize(message: Message): CloudEvent | CloudEvent[] { +function deserialize(message: Message): CloudEvent | CloudEvent[] { const cleanHeaders: Headers = sanitize(message.headers); const mode: Mode = getMode(cleanHeaders); const version = getVersion(mode, cleanHeaders, message.body); @@ -261,3 +261,14 @@ function parseBatched(message: Message): CloudEvent | CloudEvent[] { }); return ret; } + +/** + * Bindings for HTTP transport support + * @implements {@linkcode Binding} + */ + export const HTTP: Binding = { + binary, + structured, + toEvent: deserialize, + isEvent: isEvent, +}; diff --git a/src/message/index.ts b/src/message/index.ts index 94c725c2..4e1990a9 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -4,8 +4,10 @@ */ import { IncomingHttpHeaders } from "http"; -import { CloudEvent } from ".."; -import { binary, deserialize, structured, isEvent } from "./http"; +import { CloudEventV1 } from ".."; + +// reexport the HTTP protocol binding +export * from "./http"; /** * Binding is an interface for transport protocols to implement, @@ -39,11 +41,11 @@ export interface Headers extends IncomingHttpHeaders { * transport-agnostic message * @interface * @property {@linkcode Headers} `headers` - the headers for the event Message - * @property string `body` - the body of the event Message + * @property {T | string | Buffer | unknown} `body` - the body of the event Message */ -export interface Message { +export interface Message { headers: Headers; - body: string | unknown; + body: T | string | Buffer | unknown; } /** @@ -62,7 +64,7 @@ export enum Mode { * @interface */ export interface Serializer { - (event: CloudEvent): Message; + (event: CloudEventV1): Message; } /** @@ -71,7 +73,7 @@ export interface Serializer { * @interface */ export interface Deserializer { - (message: Message): CloudEvent | CloudEvent[]; + (message: Message): CloudEventV1 | CloudEventV1[]; } /** @@ -82,14 +84,3 @@ export interface Deserializer { export interface Detector { (message: Message): boolean; } - -/** - * Bindings for HTTP transport support - * @implements {@linkcode Binding} - */ -export const HTTP: Binding = { - binary: binary as Serializer, - structured: structured as Serializer, - toEvent: deserialize as Deserializer, - isEvent: isEvent as Detector, -}; diff --git a/test/integration/sdk_test.ts b/test/integration/sdk_test.ts index f31653b6..37dc4fd9 100644 --- a/test/integration/sdk_test.ts +++ b/test/integration/sdk_test.ts @@ -5,11 +5,13 @@ import "mocha"; import { expect } from "chai"; -import { CloudEvent, Version } from "../../src"; +import { CloudEvent, CloudEventV1, Version } from "../../src"; -const fixture = { +const fixture: CloudEventV1 = { + id: "123", type: "org.cloudevents.test", source: "http://cloudevents.io", + specversion: Version.V1, }; describe("The SDK Requirements", () => { @@ -34,4 +36,19 @@ describe("The SDK Requirements", () => { expect(new CloudEvent(fixture).specversion).to.equal(Version.V1); }); }); + + describe("Cloning events", () => { + it("should clone simple objects that adhere to the CloudEventV1 interface", () => { + const copy = CloudEvent.cloneWith(fixture, { id: "456" }, false); + expect(copy.id).to.equal("456"); + expect(copy.type).to.equal(fixture.type); + expect(copy.source).to.equal(fixture.source); + expect(copy.specversion).to.equal(fixture.specversion); + }); + + it("should clone simple objects with data that adhere to the CloudEventV1 interface", () => { + const copy = CloudEvent.cloneWith(fixture, { data: { lunch: "tacos" } }, false); + expect(copy.data.lunch).to.equal("tacos"); + }); + }); });