Skip to content

Commit ebc6427

Browse files
committed
Require plain json object as metadata
1 parent 01c3491 commit ebc6427

File tree

4 files changed

+79
-81
lines changed

4 files changed

+79
-81
lines changed

docs/api/appending-events.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,19 @@ This feature is only available in KurrentDB 25.1 and later.
206206
You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails.
207207

208208
::: warning
209-
Currently, metadata must be valid JSON. Binary metadata will not be supported in
210-
this version. This limitation ensures compatibility with KurrentDB's metadata
211-
handling and will be removed in the next major release.
209+
Metadata must be a valid JSON object, using string keys and string values only.
210+
Binary metadata is not supported in this version to maintain compatibility with
211+
KurrentDB's metadata handling. This restriction will be lifted in the next major
212+
release.
212213
:::
213214

214215
```ts
215216
import { jsonEvent } from "@kurrent/kurrentdb-client";
216217
import { v4 as uuid } from "uuid";
217218

218219
const metadata = {
219-
timestamp: new Date().toISOString(),
220220
source: "OrderProcessingSystem",
221-
version: 1.0
221+
version: "1.0"
222222
};
223223

224224
const requests = [

packages/db-client/src/streams/appendToStream/multiStreamAppend.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,22 @@ export const multiStreamAppend = async function (
1919
this: Client,
2020
requests: AppendStreamRequest[]
2121
): Promise<MultiAppendResult> {
22-
if (
23-
requests.some((request) =>
24-
request.events.some(
25-
(event) => event.metadata && event.metadata.constructor === Uint8Array
26-
)
27-
)
28-
)
29-
throw new Error(
30-
"multiStreamAppend requires all event metadata to be in JSON format."
31-
);
22+
for (const request of requests) {
23+
for (const event of request.events) {
24+
const { metadata } = event;
25+
if (metadata == null) continue;
26+
if (
27+
metadata.constructor === Uint8Array ||
28+
typeof metadata !== "object" ||
29+
Array.isArray(metadata) ||
30+
Object.values(metadata).some((value) => typeof value !== "string")
31+
) {
32+
throw new Error(
33+
"multiStreamAppend requires metadata to be a plain object with string keys and string values."
34+
);
35+
}
36+
}
37+
}
3238

3339
return this.execute(
3440
grpc.StreamsServiceClient,
@@ -95,7 +101,7 @@ export const multiStreamAppend = async function (
95101

96102
if (event.metadata) {
97103
const metadataMap = mapToValueMap(
98-
event.metadata as Record<string, unknown>
104+
event.metadata as Record<string, string>
99105
);
100106
for (const [key, value] of metadataMap) {
101107
record.getPropertiesMap().set(key, value);

packages/db-client/src/utils/mapToValue.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,13 @@
1-
import { NullValue, Value } from "google-protobuf/google/protobuf/struct_pb";
1+
import { Value } from "google-protobuf/google/protobuf/struct_pb";
22

3-
export const mapToValue = (source: unknown): Value => {
3+
export const mapToValue = (source: string): Value => {
44
const value = new Value();
5-
6-
if (source === null || source === undefined) {
7-
value.setNullValue(NullValue.NULL_VALUE);
8-
return value;
9-
}
10-
11-
switch (typeof source) {
12-
case "string":
13-
value.setStringValue(source);
14-
break;
15-
16-
case "boolean":
17-
value.setBoolValue(source);
18-
break;
19-
20-
case "number":
21-
value.setNumberValue(source);
22-
break;
23-
24-
case "object":
25-
if (source instanceof Date) {
26-
value.setStringValue(source.toISOString());
27-
} else if (source instanceof Uint8Array) {
28-
value.setStringValue(Buffer.from(source).toString("base64"));
29-
} else if (Buffer.isBuffer(source)) {
30-
value.setStringValue(source.toString("base64"));
31-
} else {
32-
value.setStringValue(JSON.stringify(source));
33-
}
34-
break;
35-
36-
default:
37-
value.setStringValue(String(source));
38-
break;
39-
}
40-
5+
value.setStringValue(source);
416
return value;
427
};
438

449
export const mapToValueMap = (
45-
obj: Record<string, unknown>
10+
obj: Record<string, string>
4611
): Map<string, Value> => {
4712
const map = new Map<string, Value>();
4813

packages/test/src/streams/multiAppendStream.test.ts

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
STREAM_EXISTS,
1818
WrongExpectedVersionError,
1919
StreamTombstonedError,
20+
jsonEvent,
21+
binaryEvent,
2022
} from "@kurrent/kurrentdb-client";
2123

2224
import { v4 } from "uuid";
@@ -35,36 +37,64 @@ describe("multiAppend", () => {
3537
await node.down();
3638
});
3739

38-
test("json events", async () => {
39-
const STREAM_NAME = v4().toString();
40+
optionalDescribe(supported)("Supported (>=25.1)", () => {
41+
test("invalid metadata (binary metadata)", async () => {
42+
const STREAM_NAME = v4().toString();
4043

41-
const requests: AppendStreamRequest[] = [];
44+
const requests: AppendStreamRequest[] = [];
4245

43-
requests.push({
44-
streamName: STREAM_NAME,
45-
events: binaryTestEvents(),
46-
expectedState: ANY,
47-
});
46+
requests.push({
47+
streamName: STREAM_NAME,
48+
events: [
49+
jsonEvent({
50+
type: "test",
51+
data: { key: "value" },
52+
metadata: Buffer.from("binary metadata"),
53+
}),
54+
],
55+
expectedState: ANY,
56+
});
4857

49-
try {
50-
await client.multiStreamAppend(requests);
51-
} catch (error) {
52-
expect(error).toBeInstanceOf(Error);
53-
expect(error.message).toBe(
54-
"multiStreamAppend requires server version 25.1 or higher."
58+
await expect(client.multiStreamAppend(requests)).rejects.toThrow(
59+
"multiStreamAppend requires metadata to be a plain object with string keys and string values."
5560
);
56-
}
57-
});
61+
});
62+
63+
test("invalid metadata (non plain object)", async () => {
64+
const STREAM_NAME = v4().toString();
65+
66+
const requests: AppendStreamRequest[] = [];
67+
68+
requests.push({
69+
streamName: STREAM_NAME,
70+
events: [
71+
jsonEvent({
72+
type: "test",
73+
data: {
74+
stringKey: "stringValue",
75+
numberKey: 42,
76+
},
77+
}),
78+
],
79+
expectedState: ANY,
80+
});
81+
82+
try {
83+
await client.multiStreamAppend(requests);
84+
} catch (error) {
85+
expect(error).toBeInstanceOf(Error);
86+
await expect(client.multiStreamAppend(requests)).rejects.toThrow(
87+
"multiStreamAppend requires metadata to be a plain object with string keys and string values."
88+
);
89+
}
90+
});
5891

59-
optionalDescribe(supported)("Supported (>=25.1)", () => {
6092
test("json events", async () => {
6193
const STREAM_NAME_1 = v4().toString();
6294
const STREAM_NAME_2 = v4().toString();
6395
const expectedMetadata = {
64-
timestamp: new Date().toISOString(),
65-
int: 1,
66-
float: 1.1,
67-
string: "test",
96+
name: "multiAppendTest",
97+
empty: ""
6898
};
6999

70100
const requests: AppendStreamRequest[] = [];
@@ -99,14 +129,13 @@ describe("multiAppend", () => {
99129
expect.objectContaining({
100130
"$schema.format": "Json",
101131
"$schema.name": "test",
102-
...expectedMetadata,
132+
name: "multiAppendTest",
133+
empty: ""
103134
})
104135
);
105136
}
106137
});
107-
});
108138

109-
optionalDescribe(supported)("Supported (>=25.1)", () => {
110139
test("stream deleted", async () => {
111140
const STREAM_NAME = v4().toString();
112141

@@ -126,9 +155,7 @@ describe("multiAppend", () => {
126155
expect(error).toBeInstanceOf(StreamTombstonedError);
127156
}
128157
});
129-
});
130158

131-
optionalDescribe(supported)("Supported (>=25.1)", () => {
132159
test("stream revision conflict", async () => {
133160
const STREAM_NAME = v4().toString();
134161

0 commit comments

Comments
 (0)