8000 fix: Make CloudEvent data field immutable and enumerable using Object… · cloudevents/sdk-javascript@2d5fab1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2d5fab1

Browse files
authored
fix: Make CloudEvent data field immutable and enumerable using Object.keys() (#515) (#516)
Signed-off-by: Philip Sanetra <code@psanetra.de>
1 parent c09a9cc commit 2d5fab1

File tree

8 files changed

+136
-43
lines changed

8 files changed

+136
-43
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,6 @@ typings/
9191

9292
# Package lock
9393
package-lock.json
94+
95+
# Jetbrains IDE directories
96+
.idea

examples/express-ex/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ app.post("/", (req, res) => {
2828
const responseEventMessage = new CloudEvent({
2929
source: '/',
3030
type: 'event:response',
31-
...event
31+
...event,
32+
data: {
33+
hello: 'world'
34+
}
3235
});
33-
responseEventMessage.data = {
34-
hello: 'world'
35-
};
3636

3737
// const message = HTTP.binary(responseEventMessage)
3838
const message = HTTP.structured(responseEventMessage)

src/event/cloudevent.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Emitter } from "..";
99

1010
import { CloudEventV1 } from "./interfaces";
1111
import { validateCloudEvent } from "./spec";
12-
import { ValidationError, isBinary, asBase64, isValidType } from "./validation";
12+
import { ValidationError, isBinary, asBase64, isValidType, base64AsBinary } from "./validation";
1313

1414
/**
1515
* An enum representing the CloudEvent specification version
@@ -33,7 +33,7 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
3333
dataschema?: string;
3434
subject?: string;
3535
time?: string;
36-
#_data?: T;
36+
data?: T;
3737
data_base64?: string;
3838

3939
// Extensions should not exist as it's own object, but instead
@@ -85,12 +85,21 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
8585
delete properties.dataschema;
8686

8787
this.data_base64 = properties.data_base64 as string;
88+
89+
if (this.data_base64) {
90+
this.data = base64AsBinary(this.data_base64) as unknown as T;
91+
}
92+
8893
delete properties.data_base64;
8994

9095
this.schemaurl = properties.schemaurl as string;
9196
delete properties.schemaurl;
9297

93-
this.data = properties.data;
98+
if (isBinary(properties.data)) {
99+
this.data_base64 = asBase64(properties.data as unknown as Buffer);
100+
}
101+
102+
this.data = typeof properties.data !== "undefined" ? properties.data : this.data;
94103
delete properties.data;
95104

96105
// sanity checking
@@ -127,17 +136,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
127136
Object.freeze(this);
128137
}
129138

130-
get data(): T | undefined {
131-
return this.#_data;
132-
}
133-
134-
set data(value: T | undefined) {
135-
if (isBinary(value)) {
136-
this.data_base64 = asBase64(value as unknown as Buffer);
137-
}
138-
this.#_data = value;
139-
}
140-
141139
/**
142140
* Used by JSON.stringify(). The name is confusing, but this method is called by
143141
* JSON.stringify() when converting this object to JSON.
@@ -147,7 +145,11 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
147145
toJSON(): Record<string, unknown> {
148146
const event = { ...this };
149147
event.time = new Date(this.time as string).toISOString();
150-
event.data = this.#_data;
10000 148+
149+
if (event.data_base64 && event.data) {
150+
delete event.data;
151+
}
152+
151153
return event;
152154
}
153155

@@ -230,9 +232,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
230232
event: CloudEventV1<any>,
231233
options: Partial<CloudEventV1<any>>,
232234
strict = true): CloudEvent<any> {
233-
if (event instanceof CloudEvent) {
234-
event = event.toJSON() as CloudEventV1<any>;
235-
}
236235
return new CloudEvent(Object.assign({}, event, options), strict);
237236
}
238237
}

src/event/validation.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ import { ErrorObject } from "ajv";
88
export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array |
99
Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;
1010

11+
const globalThisPolyfill = (function() {
12+
try {
13+
return globalThis;
14+
}
15+
catch (e) {
16+
try {
17+
return self;
18+
}
19+
catch (e) {
20+
return global;
21+
}
22+
}
23+
}());
1124

1225
/**
1326
* An Error class that will be thrown when a CloudEvent
@@ -86,6 +99,14 @@ export const asBuffer = (value: string | Buffer | TypeArray): Buffer =>
8699
throw new F987 TypeError("is not buffer or a valid binary");
87100
})();
88101

102+
export const base64AsBinary = (base64String: string): Uint8Array => {
103+
const toBinaryString = (base64Str: string): string => globalThisPolyfill.atob
104+
? globalThisPolyfill.atob(base64Str)
105+
: Buffer.from(base64Str, "base64").toString("binary");
106+
107+
return Uint8Array.from(toBinaryString(base64String), (c) => c.charCodeAt(0));
108+
};
109+
89110
export const asBase64 =
90111
(value: string | Buffer | TypeArray): string => asBuffer(value).toString("base64");
91112

src/message/mqtt/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { Binding, Deserializer, CloudEvent, CloudEventV1, CONSTANTS, Message, ValidationError, Headers } from "../..";
7+
import { base64AsBinary } from "../../event/validation";
78

89
export {
910
MQTT, MQTTMessageFactory
@@ -50,14 +51,16 @@ const MQTT: Binding = {
5051
* @implements {Serializer}
5152
*/
5253
function binary<T>(event: CloudEventV1<T>): MQTTMessage<T> {
53-
let properties;
54-
if (event instanceof CloudEvent) {
55-
properties = event.toJSON();
56-
} else {
57-
properties = event;
54+
const properties = { ...event };
55+
56+
let body = properties.data as T;
57+
58+
if (!body && properties.data_base64) {
59+
body = base64AsBinary(properties.data_base64) as unknown as T;
5860
}
59-
const body = properties.data as T;
61+
6062
delete properties.data;
63+
delete properties.data_base64;
6164

6265
return MQTTMessageFactory(event.datacontenttype as string, properties, body);
6366
}

test/integration/cloud_event_test.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,39 @@ import path from "path";
77
import fs from "fs";
88

99
import { expect } from "chai";
10-
import { CloudEvent, ValidationError, Version } from "../../src";
10+
import { CloudEvent, CloudEventV1, ValidationError, Version } from "../../src";
1111
import { asBase64 } from "../../src/event/validation";
1212

1313
const type = "org.cncf.cloudevents.example";
1414
const source = "http://unit.test";
1515
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
1616

17-
const fixture = {
17+
const fixture = Object.freeze({
1818
id,
1919
specversion: Version.V1,
2020
source,
2121
type,
22-
data: `"some data"`,
23-
};
22+
data: `"some data"`
23+
});
2424

2525
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
2626
const image_base64 = asBase64(imageData);
2727

28+
// Do not replace this with the assignment of a class instance
29+
// as we just want to test if we can enumerate all explicitly defined fields!
30+
const cloudEventV1InterfaceFields: (keyof CloudEventV1<unknown>)[] = Object.keys({
31+
id: "",
32+
type: "",
33+
data: undefined,
34+
data_base64: "",
35+
source: "",
36+
time: "",
37+
datacontenttype: "",
38+
dataschema: "",
39+
specversion: "",
40+
subject: ""
41+
} as Required<CloudEventV1<unknown>>);
42+
2843
describe("A CloudEvent", () => {
2944
it("Can be constructed with a typed Message", () => {
3045
const ce = new CloudEvent(fixture);
@@ -78,6 +93,58 @@ describe("A CloudEvent", () => {
7893
new CloudEvent({ ExtensionWithCaps: "extension value", ...fixture });
7994
}).throw("invalid extension name");
8095
});
96+
97+
it("CloudEventV1 interface fields should be enumerable", () => {
98+
const classInstanceKeys = Object.keys(new CloudEvent({ ...fixture }));
99+
100+
for (const key of cloudEventV1InterfaceFields) {
101+
expect(classInstanceKeys).to.contain(key);
102+
}
103+
});
104+
105+
it("throws TypeError on trying to set any field value", () => {
106+
const ce = new CloudEvent({
107+
...fixture,
108+
mycustomfield: "initialValue"
109+
});
110+
111+
const keySet = new Set([...cloudEventV1InterfaceFields, ...Object.keys(ce)]);
112+
113+
expect(keySet).not.to.be.empty;
114+
115+
for (const cloudEventKey of keySet) {
116+
let threw = false;
117+
118+
try {
119+
ce[cloudEventKey] = "newValue";
120+
} catch (err) {
121+
threw = true;
122+
expect(err).to.be.instanceOf(TypeError);
123+
expect((err as TypeError).message).to.include("Cannot assign to read only property");
124+
}
125+
126+
if (!threw) {
127+
expect.fail(`Assigning a value to ${cloudEventKey} did not throw`);
128+
}
129+
}
130+
});
131+
132+
describe("toJSON()", () => {
133+
it("does not return data field if data_base64 field is set to comply with JSON format spec 3.1.1", () => {
134+
const binaryData = new Uint8Array([1,2,3]);
135+
136+
const ce = new CloudEvent({
137+
...fixture,
138+
data: binaryData
139+
});
140+
141+
expect(ce.data).to.be.equal(binaryData);
142+
143+
const json = ce.toJSON();
144+
expect(json.data).to.not.exist;
145+
expect(json.data_base64).to.be.equal("AQID");
146+
});
147+
});
81148
});
82149

83150
describe("A 1.0 CloudEvent", () => {

test/integration/mqtt_tests.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ const ext2Name = "extension2";
3232
const ext2Value = "acme";
3333

3434
// Binary data as base64
35-
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
35+
const dataBinary = Uint8Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
3636
const data_base64 = asBase64(dataBinary);
3737

3838
// Since the above is a special case (string as binary), let's test
3939
// with a real binary file one is likely to encounter in the wild
40-
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
40+
const imageData = new Uint8Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
4141
const image_base64 = asBase64(imageData);
4242

4343
const PUBLISH = {"Content Type": "application/json; charset=utf-8"};
@@ -281,14 +281,14 @@ describe("MQTT transport", () => {
281281

282282
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
283283
const message = MQTT.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
284-
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
284+
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
285285
expect(eventDeserialized.data).to.deep.equal(imageData);
286286
expect(eventDeserialized.data_base64).to.equal(image_base64);
287287
});
288288

289289
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
290290
const message = MQTT.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
291-
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
291+
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
292292
expect(eventDeserialized.data).to.deep.equal(imageData);
293293
expect(eventDeserialized.data_base64).to.equal(image_base64);
294294
});
@@ -302,7 +302,7 @@ describe("MQTT transport", () => {
302302

303303
it("Does not parse binary data from binary messages with content type application/json", () => {
304304
const message = MQTT.binary(fixture.cloneWith({ data: dataBinary }));
305-
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
305+
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
306306
expect(eventDeserialized.data).to.deep.equal(dataBinary);
307307
expect(eventDeserialized.data_base64).to.equal(data_base64);
308308
});

test/integration/spec_1_tests.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const data = {
1919
};
2020
const subject = "subject-x0";
2121

22-
let cloudevent = new CloudEvent({
22+
const cloudevent = new CloudEvent({
2323
specversion: Version.V1,
2424
id,
2525
source,
@@ -120,8 +120,8 @@ describe("CloudEvents Spec v1.0", () => {
120120
});
121121

122122
it("defaut ID create when an empty string", () => {
123-
cloudevent = cloudevent.cloneWith({ id: "" });
124-
expect(cloudevent.id.length).to.be.greaterThan(0);
123+
const testEvent = cloudevent.cloneWith({ id: "" });
124+
expect(testEvent.id.length).to.be.greaterThan(0);
125125
});
126126
});
127127

@@ -160,11 +160,11 @@ describe("CloudEvents Spec v1.0", () => {
160160
describe("'time'", () => {
161161
it("must adhere to the format specified in RFC 3339", () => {
162162
const d = new Date();
163-
cloudevent = cloudevent.cloneWith({ time: d.toString() }, false);
163+
const testEvent = cloudevent.cloneWith({ time: d.toString() }, false);
164164
// ensure that we always get back the same thing we passed in
165-
expect(cloudevent.time).to.equal(d.toString());
165+
expect(testEvent.time).to.equal(d.toString());
166166
// ensure that when stringified, the timestamp is in RFC3339 format
167-
expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString());
167+
expect(JSON.parse(JSON.stringify(testEvent)).time).to.equal(new Date(d.toString()).toISOString());
168168
});
169169
});
170170
});

0 commit comments

Comments
 (0)
0