src(event)!: make the event's time property only a string (#330)

Previously, the event's `time` property could be either a string or a date.
this commit modifies that to ensure that the object can only be created with
a timestamp in string format. As long as the string is a valid date, that
can be parsed by `new Date(Date.parse(str))` then whenever the event is
serialized as JSON, the `time` attribute will be formatted as per RFC 3339.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/326

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2020-09-01 10:10:16 -04:00 committed by GitHub
parent f3953a9a5a
commit 6cd310c141
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 51 additions and 43 deletions

View File

@ -34,7 +34,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
datacontenttype?: string;
dataschema?: string;
subject?: string;
#_time?: string | Date;
time?: string;
#_data?: Record<string, unknown | string | number | boolean> | 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<string, unknown> {
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 {

View File

@ -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

View File

@ -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: {

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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", () => {

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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";

View File

@ -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());
});
});
});

View File

@ -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());
});
});
});