Skip to content

Commit 6cd310c

Browse files
authored
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: #326 Signed-off-by: Lance Ball <[email protected]>
1 parent f3953a9 commit 6cd310c

File tree

13 files changed

+51
-43
lines changed

13 files changed

+51
-43
lines changed

src/event/cloudevent.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
3434
datacontenttype?: string;
3535
dataschema?: string;
3636
subject?: string;
37-
#_time?: string | Date;
37+
time?: string;
3838
#_data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
3939
data_base64?: string;
4040

@@ -54,6 +54,9 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
5454
this.id = (properties.id as string) || uuidv4();
5555
delete properties.id;
5656

57+
this.time = properties.time || new Date().toISOString();
58+
delete properties.time;
59+
5760
this.type = properties.type;
5861
delete properties.type;
5962

@@ -69,9 +72,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
6972
this.subject = properties.subject;
7073
delete properties.subject;
7174

72-
this.#_time = properties.time;
73-
delete properties.time;
74-
7575
this.datacontentencoding = properties.datacontentencoding as string;
7676
delete properties.datacontentencoding;
7777

@@ -87,13 +87,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
8787
this._setData(properties.data);
8888
delete properties.data;
8989

90-
// Make sure time has a default value and whatever is provided is formatted
91-
if (!this.#_time) {
92-
this.#_time = new Date().toISOString();
93-
} else if (this.#_time instanceof Date) {
94-
this.#_time = this.#_time.toISOString();
95-
}
96-
9790
// sanity checking
9891
if (this.specversion === Version.V1 && this.schemaurl) {
9992
throw new TypeError("cannot set schemaurl on version 1.0 event");
@@ -123,14 +116,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
123116
Object.freeze(this);
124117
}
125118

126-
get time(): string | Date {
127-
return this.#_time as string | Date;
128-
}
129-
130-
set time(val: string | Date) {
131-
this.#_time = new Date(val).toISOString();
132-
}
133-
134119
get data(): unknown {
135120
if (
136121
this.datacontenttype === CONSTANTS.MIME_JSON &&
@@ -164,7 +149,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
164149
*/
165150
toJSON(): Record<string, unknown> {
166151
const event = { ...this };
167-
event.time = this.time;
152+
event.time = new Date(this.time as string).toISOString();
168153
event.data = this.data;
169154
return event;
170155
}
@@ -182,6 +167,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
182167
try {
183168
return validateCloudEvent(this);
184169
} catch (e) {
170+
console.error(e.errors);
185171
if (e instanceof ValidationError) {
186172
throw e;
187173
} else {

src/event/interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface CloudEventV1OptionalAttributes {
114114
* the same algorithm to determine the value used.
115115
* @example "2020-08-08T14:48:09.769Z"
116116
*/
117-
time?: Date | string;
117+
time?: string;
118118
/**
119119
* [OPTIONAL] The event payload. This specification does not place any restriction
120120
* on the type of this information. It is encoded into a media format which is
@@ -258,7 +258,7 @@ export interface CloudEventV03OptionalAttributes {
258258
* the same algorithm to determine the value used.
259259
* @example "2020-08-08T14:48:09.769Z"
260260
*/
261-
time?: Date | string;
261+
time?: string;
262262
/**
263263
* [OPTIONAL] The event payload. This specification does not place any restriction
264264
* on the type of this information. It is encoded into a media format which is

src/event/schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const schemaV1 = {
5656
minLength: 1,
5757
},
5858
time: {
59-
format: "date-time",
59+
format: "js-date-time",
6060
type: "string",
6161
},
6262
dataschema: {
@@ -129,7 +129,7 @@ export const schemaV03 = {
129129
minLength: 1,
130130
},
131131
time: {
132-
format: "date-time",
132+
format: "js-date-time",
133133
type: "string",
134134
},
135135
schemaurl: {

src/event/spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { Version } from "./cloudevent";
77
import CONSTANTS from "../constants";
88

99
const ajv = new Ajv({ extendRefs: true });
10+
11+
// handle date-time format specially because a user could pass
12+
// Date().toString(), which is not spec compliant date-time format
13+
ajv.addFormat("js-date-time", function (dateTimeString) {
14+
const date = new Date(Date.parse(dateTimeString));
15+
return date.toString() !== "Invalid Date";
16+
});
17+
1018
const isValidAgainstSchemaV1 = ajv.compile(schemaV1);
1119
const isValidAgainstSchemaV03 = ajv.compile(schemaV03);
1220

src/message/http/headers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function headersFor(event: CloudEvent): Headers {
7171
});
7272
// Treat time specially, since it's handled with getters and setters in CloudEvent
7373
if (event.time) {
74-
headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string;
74+
headers[CONSTANTS.CE_HEADERS.TIME] = new Date(event.time).toISOString();
7575
}
7676
return headers;
7777
}

src/parsers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ export interface MappedParser {
6666
}
6767

6868
export class DateParser extends Parser {
69-
parse(payload: string): Date {
70-
return new Date(Date.parse(payload));
69+
parse(payload: string): string {
70+
let date = new Date(Date.parse(payload));
71+
if (date.toString() === "Invalid Date") {
72+
date = new Date();
73+
}
74+
return date.toISOString();
7175
}
7276
}
7377

test/integration/cloud_event_test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ describe("A CloudEvent", () => {
2121
});
2222

2323
it("serializes as JSON with toString()", () => {
24-
const ce = new CloudEvent(fixture);
24+
const ce = new CloudEvent({ ...fixture, data: { lunch: "tacos" } });
2525
expect(ce.toString()).to.deep.equal(JSON.stringify(ce));
26+
expect(new CloudEvent(JSON.parse(ce.toString()))).to.deep.equal(ce);
27+
expect(new CloudEvent(JSON.parse(JSON.stringify(ce)))).to.deep.equal(ce);
2628
});
2729

2830
it("Throw a validation error for invalid extension names", () => {
@@ -188,9 +190,9 @@ describe("A 0.3 CloudEvent", () => {
188190
});
189191

190192
it("can be constructed with a timestamp", () => {
191-
const time = new Date();
193+
const time = new Date().toISOString();
192194
const ce = new CloudEvent({ time, ...v03fixture });
193-
expect(ce.time).to.equal(time.toISOString());
195+
expect(ce.time).to.equal(time);
194196
});
195197

196198
it("can be constructed with a datacontenttype", () => {

test/integration/http_binding_03.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const type = "com.github.pull.create";
1010
const source = "urn:event:from:myapi/resourse/123";
1111
const contentEncoding = "base64";
1212
const contentType = "application/cloudevents+json; charset=utf-8";
13-
const time = new Date();
13+
const time = new Date().toISOString();
1414
const schemaurl = "http://cloudevents.io/schema.json";
1515

1616
const ceContentType = "application/json";

test/integration/http_binding_1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { AxiosResponse } from "axios";
1111
const type = "com.github.pull.create";
1212
const source = "urn:event:from:myapi/resource/123";
1313
const contentType = "application/cloudevents+json; charset=utf-8";
14-
const time = new Date();
14+
const time = new Date().toISOString();
1515
const subject = "subject.ext";
1616
const dataschema = "http://cloudevents.io/schema.json";
1717
const datacontenttype = "application/json";

test/integration/http_emitter_test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => {
4545
const event = new CloudEvent({
4646
type,
4747
source,
48-
time: new Date(),
48+
time: new Date().toISOString(),
4949
data,
5050
[ext1Name]: ext1Value,
5151
[ext2Name]: ext2Value,
@@ -143,7 +143,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => {
143143
specversion: Version.V03,
144144
type,
145145
source,
146-
time: new Date(),
146+
time: new Date().toISOString(),
147147
data,
148148
[ext1Name]: ext1Value,
149149
[ext2Name]: ext2Value,

test/integration/message_test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Message, HTTP } from "../../src/message";
55

66
const type = "org.cncf.cloudevents.example";
77
const source = "urn:event:from:myapi/resource/123";
8-
const time = new Date();
8+
const time = new Date().toISOString();
99
const subject = "subject.ext";
1010
const dataschema = "http://cloudevents.io/schema.json";
1111
const datacontenttype = "application/json";

test/integration/spec_03_tests.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Constants from "../../src/constants";
66
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
77
const type = "com.github.pull.create";
88
const source = "urn:event:from:myapi/resourse/123";
9-
const time = new Date();
9+
const time = new Date().toISOString();
1010
const schemaurl = "http://example.com/registry/myschema.json";
1111
const data = {
1212
much: "wow",
@@ -68,7 +68,7 @@ describe("CloudEvents Spec v0.3", () => {
6868
});
6969

7070
it("Should have 'time'", () => {
71-
expect(cloudevent.time).to.equal(time.toISOString());
71+
expect(cloudevent.time).to.equal(time);
7272
});
7373

7474
it("Should have 'data'", () => {
@@ -186,8 +186,12 @@ describe("CloudEvents Spec v0.3", () => {
186186

187187
describe("'time'", () => {
188188
it("must adhere to the format specified in RFC 3339", () => {
189-
cloudevent = cloudevent.cloneWith({ time: time });
190-
expect(cloudevent.time).to.equal(time.toISOString());
189+
const d = new Date();
190+
cloudevent = cloudevent.cloneWith({ time: d.toString() });
191+
// ensure that we always get back the same thing we passed in
192+
expect(cloudevent.time).to.equal(d.toString());
193+
// ensure that when stringified, the timestamp is in RFC3339 format
194+
expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString());
191195
});
192196
});
193197
});

test/integration/spec_1_tests.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Constants from "../../src/constants";
77
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
88
const type = "com.github.pull.create";
99
const source = "urn:event:from:myapi/resourse/123";
10-
const time = new Date();
10+
const time = new Date().toISOString();
1111
const dataschema = "http://example.com/registry/myschema.json";
1212
const data = {
1313
much: "wow",
@@ -59,7 +59,7 @@ describe("CloudEvents Spec v1.0", () => {
5959
});
6060

6161
it("Should have 'time'", () => {
62-
expect(cloudevent.time).to.equal(time.toISOString());
62+
expect(cloudevent.time).to.equal(time);
6363
});
6464
});
6565

@@ -144,8 +144,12 @@ describe("CloudEvents Spec v1.0", () => {
144144

145145
describe("'time'", () => {
146146
it("must adhere to the format specified in RFC 3339", () => {
147-
cloudevent = cloudevent.cloneWith({ time: time });
148-
expect(cloudevent.time).to.equal(time.toISOString());
147+
const d = new Date();
148+
cloudevent = cloudevent.cloneWith({ time: d.toString() });
149+
// ensure that we always get back the same thing we passed in
150+
expect(cloudevent.time).to.equal(d.toString());
151+
// ensure that when stringified, the timestamp is in RFC3339 format
152+
expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString());
149153
});
150154
});
151155
});

0 commit comments

Comments
 (0)