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:
parent
f3953a9a5a
commit
6cd310c141
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue