diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 197b4bc1..33e1df20 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -34,7 +34,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { datacontenttype?: string; dataschema?: string; subject?: string; - #_time?: string | Date; + time?: string; #_data?: Record | string | number | boolean | null | unknown; data_base64?: string; @@ -54,6 +54,9 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.id = (properties.id as string) || uuidv4(); delete properties.id; + this.time = properties.time || new Date().toISOString(); + delete properties.time; + this.type = properties.type; delete properties.type; @@ -69,9 +72,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.subject = properties.subject; delete properties.subject; - this.#_time = properties.time; - delete properties.time; - this.datacontentencoding = properties.datacontentencoding as string; delete properties.datacontentencoding; @@ -87,13 +87,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this._setData(properties.data); delete properties.data; - // Make sure time has a default value and whatever is provided is formatted - if (!this.#_time) { - this.#_time = new Date().toISOString(); - } else if (this.#_time instanceof Date) { - this.#_time = this.#_time.toISOString(); - } - // sanity checking if (this.specversion === Version.V1 && this.schemaurl) { throw new TypeError("cannot set schemaurl on version 1.0 event"); @@ -123,14 +116,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { Object.freeze(this); } - get time(): string | Date { - return this.#_time as string | Date; - } - - set time(val: string | Date) { - this.#_time = new Date(val).toISOString(); - } - get data(): unknown { if ( this.datacontenttype === CONSTANTS.MIME_JSON && @@ -164,7 +149,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { */ toJSON(): Record { const event = { ...this }; - event.time = this.time; + event.time = new Date(this.time as string).toISOString(); event.data = this.data; return event; } @@ -182,6 +167,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { try { return validateCloudEvent(this); } catch (e) { + console.error(e.errors); if (e instanceof ValidationError) { throw e; } else { diff --git a/src/event/interfaces.ts b/src/event/interfaces.ts index f886cb1f..bfc3a0e9 100644 --- a/src/event/interfaces.ts +++ b/src/event/interfaces.ts @@ -114,7 +114,7 @@ export interface CloudEventV1OptionalAttributes { * the same algorithm to determine the value used. * @example "2020-08-08T14:48:09.769Z" */ - time?: Date | string; + time?: string; /** * [OPTIONAL] The event payload. This specification does not place any restriction * on the type of this information. It is encoded into a media format which is @@ -258,7 +258,7 @@ export interface CloudEventV03OptionalAttributes { * the same algorithm to determine the value used. * @example "2020-08-08T14:48:09.769Z" */ - time?: Date | string; + time?: string; /** * [OPTIONAL] The event payload. This specification does not place any restriction * on the type of this information. It is encoded into a media format which is diff --git a/src/event/schemas.ts b/src/event/schemas.ts index 51d21e14..c27e44bf 100644 --- a/src/event/schemas.ts +++ b/src/event/schemas.ts @@ -56,7 +56,7 @@ export const schemaV1 = { minLength: 1, }, time: { - format: "date-time", + format: "js-date-time", type: "string", }, dataschema: { @@ -129,7 +129,7 @@ export const schemaV03 = { minLength: 1, }, time: { - format: "date-time", + format: "js-date-time", type: "string", }, schemaurl: { diff --git a/src/event/spec.ts b/src/event/spec.ts index 537b6432..3c3abc31 100644 --- a/src/event/spec.ts +++ b/src/event/spec.ts @@ -7,6 +7,14 @@ import { Version } from "./cloudevent"; import CONSTANTS from "../constants"; const ajv = new Ajv({ extendRefs: true }); + +// handle date-time format specially because a user could pass +// Date().toString(), which is not spec compliant date-time format +ajv.addFormat("js-date-time", function (dateTimeString) { + const date = new Date(Date.parse(dateTimeString)); + return date.toString() !== "Invalid Date"; +}); + const isValidAgainstSchemaV1 = ajv.compile(schemaV1); const isValidAgainstSchemaV03 = ajv.compile(schemaV03); diff --git a/src/message/http/headers.ts b/src/message/http/headers.ts index 228445cb..fcb65e52 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -71,7 +71,7 @@ export function headersFor(event: CloudEvent): Headers { }); // Treat time specially, since it's handled with getters and setters in CloudEvent if (event.time) { - headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; + headers[CONSTANTS.CE_HEADERS.TIME] = new Date(event.time).toISOString(); } return headers; } diff --git a/src/parsers.ts b/src/parsers.ts index f97bfed9..bbfe128c 100644 --- a/src/parsers.ts +++ b/src/parsers.ts @@ -66,8 +66,12 @@ export interface MappedParser { } export class DateParser extends Parser { - parse(payload: string): Date { - return new Date(Date.parse(payload)); + parse(payload: string): string { + let date = new Date(Date.parse(payload)); + if (date.toString() === "Invalid Date") { + date = new Date(); + } + return date.toISOString(); } } diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index 70881c96..e4fa1abc 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -21,8 +21,10 @@ describe("A CloudEvent", () => { }); it("serializes as JSON with toString()", () => { - const ce = new CloudEvent(fixture); + const ce = new CloudEvent({ ...fixture, data: { lunch: "tacos" } }); expect(ce.toString()).to.deep.equal(JSON.stringify(ce)); + expect(new CloudEvent(JSON.parse(ce.toString()))).to.deep.equal(ce); + expect(new CloudEvent(JSON.parse(JSON.stringify(ce)))).to.deep.equal(ce); }); it("Throw a validation error for invalid extension names", () => { @@ -188,9 +190,9 @@ describe("A 0.3 CloudEvent", () => { }); it("can be constructed with a timestamp", () => { - const time = new Date(); + const time = new Date().toISOString(); const ce = new CloudEvent({ time, ...v03fixture }); - expect(ce.time).to.equal(time.toISOString()); + expect(ce.time).to.equal(time); }); it("can be constructed with a datacontenttype", () => { diff --git a/test/integration/http_binding_03.ts b/test/integration/http_binding_03.ts index aae7f3b7..c3dc2f57 100644 --- a/test/integration/http_binding_03.ts +++ b/test/integration/http_binding_03.ts @@ -10,7 +10,7 @@ const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resourse/123"; const contentEncoding = "base64"; const contentType = "application/cloudevents+json; charset=utf-8"; -const time = new Date(); +const time = new Date().toISOString(); const schemaurl = "http://cloudevents.io/schema.json"; const ceContentType = "application/json"; diff --git a/test/integration/http_binding_1.ts b/test/integration/http_binding_1.ts index d64cd75e..a5a0decf 100644 --- a/test/integration/http_binding_1.ts +++ b/test/integration/http_binding_1.ts @@ -11,7 +11,7 @@ import { AxiosResponse } from "axios"; const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resource/123"; const contentType = "application/cloudevents+json; charset=utf-8"; -const time = new Date(); +const time = new Date().toISOString(); const subject = "subject.ext"; const dataschema = "http://cloudevents.io/schema.json"; const datacontenttype = "application/json"; diff --git a/test/integration/http_emitter_test.ts b/test/integration/http_emitter_test.ts index e9f184bc..a2d9c051 100644 --- a/test/integration/http_emitter_test.ts +++ b/test/integration/http_emitter_test.ts @@ -45,7 +45,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { const event = new CloudEvent({ type, source, - time: new Date(), + time: new Date().toISOString(), data, [ext1Name]: ext1Value, [ext2Name]: ext2Value, @@ -143,7 +143,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { specversion: Version.V03, type, source, - time: new Date(), + time: new Date().toISOString(), data, [ext1Name]: ext1Value, [ext2Name]: ext2Value, diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index c6a3315f..c8966af1 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -5,7 +5,7 @@ import { Message, HTTP } from "../../src/message"; const type = "org.cncf.cloudevents.example"; const source = "urn:event:from:myapi/resource/123"; -const time = new Date(); +const time = new Date().toISOString(); const subject = "subject.ext"; const dataschema = "http://cloudevents.io/schema.json"; const datacontenttype = "application/json"; diff --git a/test/integration/spec_03_tests.ts b/test/integration/spec_03_tests.ts index c0732758..84252a4a 100644 --- a/test/integration/spec_03_tests.ts +++ b/test/integration/spec_03_tests.ts @@ -6,7 +6,7 @@ import Constants from "../../src/constants"; const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838"; const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); +const time = new Date().toISOString(); const schemaurl = "http://example.com/registry/myschema.json"; const data = { much: "wow", @@ -68,7 +68,7 @@ describe("CloudEvents Spec v0.3", () => { }); it("Should have 'time'", () => { - expect(cloudevent.time).to.equal(time.toISOString()); + expect(cloudevent.time).to.equal(time); }); it("Should have 'data'", () => { @@ -186,8 +186,12 @@ describe("CloudEvents Spec v0.3", () => { describe("'time'", () => { it("must adhere to the format specified in RFC 3339", () => { - cloudevent = cloudevent.cloneWith({ time: time }); - expect(cloudevent.time).to.equal(time.toISOString()); + const d = new Date(); + cloudevent = cloudevent.cloneWith({ time: d.toString() }); + // ensure that we always get back the same thing we passed in + expect(cloudevent.time).to.equal(d.toString()); + // ensure that when stringified, the timestamp is in RFC3339 format + expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString()); }); }); }); diff --git a/test/integration/spec_1_tests.ts b/test/integration/spec_1_tests.ts index e39d3a56..9329dbd0 100644 --- a/test/integration/spec_1_tests.ts +++ b/test/integration/spec_1_tests.ts @@ -7,7 +7,7 @@ import Constants from "../../src/constants"; const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838"; const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); +const time = new Date().toISOString(); const dataschema = "http://example.com/registry/myschema.json"; const data = { much: "wow", @@ -59,7 +59,7 @@ describe("CloudEvents Spec v1.0", () => { }); it("Should have 'time'", () => { - expect(cloudevent.time).to.equal(time.toISOString()); + expect(cloudevent.time).to.equal(time); }); }); @@ -144,8 +144,12 @@ describe("CloudEvents Spec v1.0", () => { describe("'time'", () => { it("must adhere to the format specified in RFC 3339", () => { - cloudevent = cloudevent.cloneWith({ time: time }); - expect(cloudevent.time).to.equal(time.toISOString()); + const d = new Date(); + cloudevent = cloudevent.cloneWith({ time: d.toString() }); + // ensure that we always get back the same thing we passed in + expect(cloudevent.time).to.equal(d.toString()); + // ensure that when stringified, the timestamp is in RFC3339 format + expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString()); }); }); });